Guida interattiva a
Positioning, layering, overflow, sticky e hidden content
Finora abbiamo conosciuto tre sistemi di layout: Flow, Flexbox e Grid. Hanno tutti una cosa in comune: cercano in ogni modo di evitare che gli elementi si sovrappongano. Ogni scatola ha il suo spazio, e i vicini si spostano per farle posto.
In questa lezione studiamo l'ultimo sistema di layout CSS: il Positioned Layout.
Qui le regole cambiano. Gli elementi possono sovrapporsi, uscire dai loro contenitori, rimanere ancorati al viewport mentre la pagina scorre, e spostarsi senza costringere i vicini a cedere spazio.
position: relative, absolute, fixed, stickyz-index e gli stacking contextoverflow e gli scroll containertransformNella lezione di Grid abbiamo già incontrato di sfuggita z-index, sticky e overflow come "trucchi" per aggiustare la griglia. Oggi li studiamo come strumenti generali del browser, non come eccezioni di Grid.
Riprendiamo la panoramica che abbiamo già visto, e spostiamo i riflettori.
display: block / display: inline
Default del browser. Gli elementi si impilano, nessuna sovrapposizione.
display: flex
Layout mono-dimensionale, elementi che si adattano al contenuto.
display: grid
Griglia bidimensionale con righe e colonne definite.
position: relative | absolute | fixed | sticky
Elementi che possono sovrapporsi, uscire dal flusso, rimanere ancorati durante lo scroll. Oggi tocca a lui!
Ogni valore di position è una specie di "mini-algoritmo di layout dentro l'algoritmo di layout". In questa lezione li vediamo uno per uno.
Alla fine della lezione aggiungiamo un bonus: le transform, che non sono un sistema di layout ma sono lo strumento che completa il toolkit per muovere e trasformare gli elementi.
Entriamo in Positioned Layout scrivendo la proprietà position su un elemento:
.box {
position: relative;
}
I valori che attivano Positioned Layout sono quattro:
relative — l'elemento resta nel suo posto naturale, ma sblocca una serie di superpoteriabsolute — l'elemento esce dal flusso e si piazza dove vogliamo noifixed — l'elemento resta ancorato al viewport (la finestra del browser), anche durante lo scrollsticky — un ibrido: scorre con la pagina fino a un punto, poi si "incolla"Ognuno di questi quattro valori si comporta in modo diverso e ha le sue regole. Li vedremo uno alla volta.
Il valore di default della proprietà position è static. Un elemento statically-positioned (posizionato staticamente) è semplicemente un elemento che non usa Positioned Layout: sta usando Flow, Flexbox o Grid come sempre.
Se voglio tornare indietro da Positioned Layout al comportamento normale, scrivo:
.box {
position: static;
/* oppure: position: initial; */
}
<div class="row">
<div class="box">1</div>
<div class="box highlight">2</div>
<div class="box">3</div>
</div>
position: staticDefault del browser: l'elemento resta nel flusso normale.
position: relativeEntrate in Positioned Layout: ora potete usare offset e layering.
"Statically-positioned" è un modo un po' fuorviante di dire "non-posizionato". Non è un sistema di posizionamento, è l'assenza di position.
position: relative - Sbloccare gli Offsetrelative è la variante più tranquilla di Positioned Layout. Di solito potete scrivere position: relative su un elemento e, a prima vista, non cambia niente. Sembra che non faccia nulla!
In realtà fa due cose:
Entrando in Positioned Layout, diventano attivi quattro nuovi "direttori di movimento":
.box {
position: relative;
top: 20px;
left: 40px;
right: 0;
bottom: 0;
}
Queste proprietà — top, left, right, bottom — sono dei "riferimenti direzionali" che indicano di quanto spostare l'elemento.
Con position: relative, gli offset sono misurati rispetto al punto dove l'elemento sarebbe stato normalmente. Ecco perché si chiama relative: relativo alla propria posizione naturale.
<div class="row">
<div class="box">1</div>
<div class="box">2</div>
<div class="box pink">3</div>
<div class="box">4</div>
<div class="box">5</div>
</div>
.pink {
position: relative;
top: 0px;
left: 0px;
}
Da notare: mentre spostate la box rosa, le box grigie intorno a lei non si muovono. La rosa si sposta, ma il layout che la circonda resta identico.
I valori negativi funzionano benissimo: left: -10px produce lo stesso effetto visivo di right: 10px. Scegliete quello che si legge meglio nel vostro caso.
Prima di scorrere il demo, provate a predire: se spostate la box rosa di 20px verso il basso, le box nere sotto di lei si spostano anch'esse? Pensateci un momento, poi guardate il confronto.
A questo punto potreste pensare: "Fico, ma io posso già spostare un elemento con margin. Che differenza c'è?"
La differenza è enorme, ed è la chiave per capire tutto Positioned Layout:
margin fa parte del calcolo del layout: se sposto un elemento con margin-top, tutto quello che gli sta intorno si riorganizza per tenerne conto.top/left/right/bottom in Positioned Layout sono cosmetici: spostano l'elemento dopo che il layout è già stato calcolato. Nessun vicino si muove.<div class="stack">
<div class="box pink">A</div>
<div class="box dark">B</div>
<div class="box dark">C</div>
</div>
/* niente */
margin-top.pink {
margin-top: 20px;
}
B e C scendono con A.
position: relative.pink {
position: relative;
top: 20px;
}
B e C non si muovono.
Con margin-top, le box sotto sono come una fila di auto costrette a fare retromarcia per lasciar passare un camion che sta svoltando: tutte devono spostarsi. Con top, la box rosa si muove da sola, come un fantasma che attraversa il muro senza disturbare nessuno.
C'è un altro effetto collaterale di margin che left non ha. Se un elemento block non ha una larghezza specificata, margin-left può restringerlo (perché la larghezza si calcola in base allo spazio rimasto). left, invece, sposta la scatola così com'è: la larghezza non cambia, e l'elemento può "uscire" dal contenitore.
Più avanti vedremo una terza opzione per spostare gli elementi senza toccare il layout circostante: le transform. Hanno uno scopo simile ma un sistema di riferimento diverso. Le confronteremo esplicitamente nell'ultima sezione della lezione.
Nota finale: relative funziona anche su elementi inline! È utile per piccoli "nudge" tipografici, come alzare di qualche pixel una parola in <strong> all'interno di un paragrafo senza rompere l'allineamento delle altre righe.
position: absolute - Rompere il FlussoFinora tutti gli elementi si sono comportati in modo "ordinato": uno sotto l'altro, ognuno con il suo spazio. Adesso facciamo qualcosa di radicale: prendiamo un elemento e lo stacchiamo dal flusso per piazzarlo dove vogliamo.
Questa è la specialità di position: absolute.
.pink-box {
position: absolute;
top: 0px;
right: 0px;
}
relativetop/left/right/bottom non sono più relativi alla posizione naturale. Sono distanze misurate dai bordi del contenitore (vedremo fra poco quale contenitore, esattamente).HTML di partenza:
<div class="frame">
<p>Paragrafo uno.</p>
<p>Paragrafo due.</p>
<p>Paragrafo tre.</p>
<div class="pink-box"></div>
</div>
Paragrafo uno.
Paragrafo due.
Paragrafo tre.
.pink-box {
position: absolute;
top: 20px;
left: 25%;
}
absolute?È lo strumento giusto per elementi che devono galleggiare sopra il contenuto:
In tutti questi casi, vogliamo che l'elemento non influenzi il layout intorno.
Quando un elemento ha position: absolute, per il browser è come se non esistesse più durante il calcolo del layout. È un fantasma, un ologramma: potete attraversarlo con la mano.
Spostare un elemento assolutamente posizionato dentro l'HTML non cambia il risultato visivo. Se ha top: 0; right: 0, andrà sempre in alto a destra, che sia il primo o l'ultimo figlio.
Se scrivo position: absolute ma non specifico nessuno fra top/left/right/bottom, l'elemento resta dove sarebbe stato in Flow layout. Ma è comunque fuori dal flusso, quindi si sovrappone agli elementi che lo seguono.
Tutti gli elementi restano nel flusso, uno sotto l'altro.
absolute senza anchorLa box rosa resta nel suo punto naturale, ma il terzo elemento le passa sotto.
Osservate questo esempio:
<div class="parent">
<div class="child"></div>
</div>
Se .child è l'unico figlio e gli mettiamo position: absolute, il genitore .parent collassa a zero altezza. Per il layout del genitore, il figlio è sparito.
Da notare: con OFF il genitore è alto 200px (l'altezza del figlio). Con ON il figlio è absolute, il genitore collassa e resta alto solo quanto i suoi bordi.
...probabilmente state usando absolute al posto sbagliato. absolute esiste apposta per lavorare fuori dal layout: se vi serve che il layout tenga conto dell'elemento, usate Flexbox, Grid o Flow.
absolute o con Grid?Nella lezione di Grid avete visto una cosa interessante: più elementi possono finire nella stessa cella e sovrapporsi, senza position: absolute. Quindi adesso abbiamo due tecniche per far sovrapporre elementi. Quando usare quale?
<section class="hero">
<div class="hero-image">Immagine</div>
<h3 class="hero-title">Titolo sopra l'immagine</h3>
</section>
.hero {
display: grid;
}
.hero > * {
grid-area: 1 / 1;
}
position: absolute.hero {
position: relative;
}
.hero-title {
position: absolute;
bottom: 16px;
left: 16px;
}
position: absolute quando l'elemento è anchored UI (badge, close button, tooltip, dropdown, popover, decorazioni) e non deve riservare spazio nel layout.Nelle UI reali li combinate: Grid per il layout principale, absolute per gli accessori ancorati ai componenti.
Quando scrivete top: 0; left: 0 su un elemento absolute, il browser lo piazza nell'angolo in alto a sinistra... di cosa, esattamente?
Di un rettangolo di riferimento chiamato containing block (letteralmente, "blocco contenitore").
In Flow layout, il containing block è semplice: è il content box del genitore diretto.
Con position: absolute, le regole cambiano. Quando un elemento dice "io sono absolute, chi è il mio riferimento?", il browser sale l'albero del DOM e si ferma al primo antenato con una position diversa da static. Quello è il containing block.
absoluteposition diversa da static (relative, absolute, fixed, sticky)<div class="level-1">
<div class="level-2">
<div class="level-3">
<div class="pink"></div>
</div>
</div>
</div>
relative:
.level-1 { position: relative; }
.pink {
position: absolute;
top: 0;
right: 0;
}
Questo è il meccanismo più comune per "contenere" un elemento absolute: si dà position: relative al genitore. Il genitore non si muove (ricordate: relative da solo è invisibile) ma ora offre un containing block al figlio.
Il padding del containing block non viene rispettato dall'elemento absolute. La box rosa si appiccica al bordo, non al content box. Il padding fa parte del Flow layout, e gli elementi absolute sono fuori dal flusso.
Gli elementi absolute hanno un altro trucco nella manica: possiamo centrarli perfettamente dentro il loro containing block.
.box {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100px;
height: 100px;
margin: auto;
}
Serve tutto e quattro:
position: absolute0width e una height esplicitemargin: autoIl browser vede i quattro 0 e capisce: "vuole stare centrato in entrambe le dimensioni". margin: auto distribuisce lo spazio residuo equamente.
<div class="frame">
<div class="box"></div>
</div>
Sì! Anche con Flexbox e Grid, questo trucco è utilissimo per UI "galleggianti" come modal, dialog e drawer, che stanno sopra al resto della pagina con position: fixed (lo vediamo nella sezione 3).
insetScrivere top: 0; left: 0; right: 0; bottom: 0 ogni volta è noioso. CSS moderno ci dà inset:
.box {
position: absolute;
inset: 0; /* equivalente ai 4 valori sopra */
width: 100px;
height: 100px;
margin: auto;
}
inset imposta tutti e quattro gli offset in un colpo solo. Accetta anche valori diversi (come margin o padding):
.box {
position: absolute;
inset: 25px; /* 25px da tutti i lati */
}
inset è supportato da tutti i browser moderni (oltre il 95% degli utenti). Usatela pure come default: è più corta e più leggibile di quattro righe separate.
Aprite CodePen e incollate questo codice. La vostra missione: far finire la box rosa nell'angolo alto-destro delle singole scatole indicate, cambiando solo la proprietà position delle scatole genitori (non toccate la box rosa).
<!-- Scenario 1: far finire la rosa nell'angolo del frame ESTERNO -->
<div class="frame">
<div class="frame">
<div class="pink-box"></div>
</div>
</div>
<!-- Scenario 2: far finire la rosa nell'angolo del frame INTERNO -->
<div class="frame">
<div class="frame">
<div class="pink-box"></div>
</div>
</div>
.frame {
padding: 16px;
border: 2px solid silver;
margin-bottom: 12px;
}
.pink-box {
position: absolute;
top: 0;
right: 0;
width: 40px;
height: 40px;
background: deeppink;
}
position: relative solo al frame esterno di turno. Dove finisce la box?position: relative solo al frame interno. La box si sposta?position: relative a entrambi i frame. Chi vince? (ricordate: il primo antenato posizionato che incontra salendo)position dai frame. Dove finisce ora la box? (suggerimento: pensate al viewport)padding: 16px a un frame con position: relative. Il padding viene rispettato dalla box rosa?Abbiamo visto che gli elementi absolute possono sovrapporsi fra loro e agli altri elementi. Ma adesso arriva la domanda vera:
Quando due elementi occupano gli stessi pixel, chi "vince"? Chi viene disegnato sopra?
La risposta breve è: dipende dal layout mode e dall'ordine nel DOM. La risposta lunga occupa questa intera sezione.
Nella lezione di Grid avete scoperto una piccola bizzarria: i grid children possono usare z-index anche senza position, e ancora meglio, z-index su un grid child crea uno stacking context. Vi avevo promesso che ne avremmo riparlato. Il momento è adesso.
In questa sezione costruiamo il modello generale del browser per la sovrapposizione, e dentro questo modello la regola di Grid diventa un caso particolare del tutto coerente.
z-index e le sue regoleisolation: l'antidoto pulito contro le "z-index wars"Quando due elementi occupano gli stessi pixel, il browser segue un ordine di pittura ben preciso. La regola di base, semplificata per ora:
Gli elementi posizionati sono sempre disegnati sopra quelli non-posizionati.
Un elemento è posizionato se ha position: relative, absolute, fixed o sticky. È non-posizionato se è in Flow, Flexbox, Grid senza position, oppure ha position: static.
<style>
.box {
width: 50px;
height: 50px;
background: silver;
}
.second {
margin-top: -30px; /* forziamo la sovrapposizione */
margin-left: 20px;
background: hotpink;
}
</style>
<div class="box first">A</div>
<div class="box second">B</div>
La box B (rosa) è sopra, perché viene dopo nel DOM.
A posizionataLa box A (argento) sale sopra, perché ora è posizionata mentre B non lo è.
relativeTornano nell'ordine del DOM; B di nuovo sopra.
Nel Flow layout c'è un dettaglio in più: il contenuto (testo, immagini) viene dipinto separatamente dallo sfondo. Può succedere che la lettera di una box "galleggi" sopra lo sfondo di un'altra anche se la box come scatola sta sotto. È un indizio in più che Flow layout non è pensato per gestire livelli: per quello serve Positioned Layout.
z-index - L'Asse ZCosa succede quando l'ordine del DOM non basta? Ad esempio, quando la box che deve stare sopra non è l'ultima nel DOM per motivi di accessibilità o di struttura semantica?
Per questo esiste la proprietà z-index.
.first.box {
position: relative;
z-index: 2;
}
.second.box {
position: relative;
z-index: 1;
}
La z si riferisce al terzo asse: oltre a x (orizzontale) e y (verticale), il browser considera anche una profondità. Valori più alti di z-index "emergono" verso lo schermo; valori più bassi affondano.
z-indexz-index funziona lo stesso). Nel Flow layout puro non ha effetto.z-index: 1.5 non è valido.auto. auto non è uguale a 0: un elemento con z-index: auto partecipa all'ordine di disegno del suo stacking context genitore senza crearne uno nuovo. Un elemento posizionato con z-index: 0 invece crea un nuovo stacking context (ne parliamo fra poco). Nei casi semplici sembrano identici; la differenza emerge appena un figlio usa z-index. Qualsiasi valore positivo "promuove" un elemento sopra i fratelli senza z-index.z-index: -1), ma portano più grattacapi che benefici. In questa lezione non li usiamo.<div class="box first">A</div>
<div class="box second">B</div>
.first.box {
position: relative;
z-index: 0;
}
.second.box {
position: relative;
}
Slider z-index sulla prima box (da 0 a 3). La seconda box resta senza z-index: vedete il cambio di livello in tempo reale.
Nella lezione di Grid avete visto che z-index funziona sui grid children anche senza position. Non è un'eccezione casuale: l'algoritmo di Grid (come quello di Flexbox) usa z-index nello stesso modo di Positioned Layout. Quindi la regola didattica "z-index funziona con elementi posizionati" va letta così: z-index funziona quando l'elemento partecipa a un layout che lo supporta — Positioned, Flexbox, Grid. Il Flow layout puro resta escluso.
Finora abbiamo pensato a z-index come a un "ordine globale": un numero più alto vince sempre. In realtà non è così.
Definizione: uno stacking context (letteralmente "contesto di impilamento") è un gruppo di elementi che vengono confrontati fra loro in fase di disegno. I valori di z-index hanno senso solo dentro lo stesso stacking context.
Pensatelo come un condominio: z-index: 99 al quinto piano del palazzo A non ha alcun effetto su chi sta al primo piano del palazzo B. Sono due classifiche separate — la classifica del palazzo A e quella del palazzo B non si mescolano mai.
Detta in pochissime parole: z-index non è una classifica globale. È una classifica locale dentro il suo stacking context. Tenere questo a mente risolve il 90% dei bug di z-index.
Un nuovo stacking context nasce quando un elemento soddisfa certe condizioni. Le cause più comuni (quelle che vedrete nei progetti reali):
position posizionato + z-index con valore diverso da auto (la combinazione classica: ricordate che z-index: 0 crea il context, z-index: auto no)position: fixed o position: sticky (da sole, senza bisogno di z-index)opacity minore di 1 (sì, anche solo opacity: 0.99 crea un context!)transform con qualunque valore (vedremo perché nell'ultima sezione)isolation: isolate (lo strumento che vedremo fra poco, pensato apposta per questo)C'è una lista completa su MDN, ma queste cinque coprono praticamente tutti i casi reali.
Quando un elemento crea uno stacking context, tutti i suoi discendenti che usano z-index vengono "schiacciati" dentro quel context. I loro z-index non possono più scavalcare elementi che stanno fuori dal context del genitore.
<div style="position: relative; z-index: 1;"> <!-- crea un context -->
<div style="z-index: 999">
Sono schiacciato dentro il context del genitore.
Non posso scavalcare elementi fuori dal genitore.
</div>
</div>
<div style="position: relative; z-index: 2;">
<!-- Questo sta sopra l'intero context con z-index:1,
anche se dentro c'è un z-index:999 -->
</div>
Da notare: la box rosa prova a uscire a destra, ma resta comunque sotto al context scuro, perché il suo z-index vale solo dentro il context giallo.
z-index: 999 dentro un context con z-index: 1 non batte un context con z-index: 2. Il figlio è prigioniero del genitore — la sua classifica è solo locale.
z-index: 999999 Non BastaÈ lo scenario ricorrente più frustrante del CSS. Avete un elemento che deve stare sopra; alzate il suo z-index. Non basta, lo alzate ancora. E ancora. Siete a z-index: 99999 e ancora non funziona. Questa è la "z-index war": una guerra di numeri sempre più grandi contro un problema che non è un problema di numeri — è un problema di stacking context.
Una pagina ha:
position: fixed; z-index: 2.card { position: relative; z-index: 1 }, e la card centrale con z-index: 2 per emergere sopra le altreTutto sembra funzionare. Poi arriva la segnalazione: scrollando, l'header passa "in mezzo" alle card. Sotto la card centrale, sopra le laterali. Un disastro.
Perché header e card sono tutti nello stesso stacking context (quello della root della pagina). I loro z-index si confrontano tra loro:
z-index: 1) → sotto l'header (z-index: 2). OK.z-index: 2) → pari con l'header, e vince per ordine nel DOM. Bug!La soluzione sbagliata: alzare ancora il z-index dell'header. Funziona oggi, fallisce domani.
La soluzione giusta: fare in modo che le tre card vivano in uno stacking context isolato, così i loro z-index interni non possano più confrontarsi con quello dell'header.
isolation - Creare un Context Senza Effetti CollateraliAbbiamo detto che position: relative; z-index: 1 sul contenitore delle card crea uno stacking context e risolve il bug:
.pricing {
position: relative;
z-index: 1;
}
Funziona, ma ha uno svantaggio: siamo costretti a scegliere un numero (z-index: 1) e a dichiarare che il contenitore è posizionato, anche se non ci serve per altri motivi.
La proprietà isolation fa esattamente — e soltanto — quella cosa lì:
.pricing {
isolation: isolate;
}
Un solo valore, un solo effetto: crea uno stacking context. Non richiede position, non richiede un numero, non ha effetti collaterali sul layout.
<header class="page-header">Header</header>
<section class="pricing">
<article class="card">Starter</article>
<article class="card primary">Pro</article>
<article class="card">Enterprise</article>
</section>
.pricing:
.pricing {
/* isolation: isolate; */
}
Scena che riproduce il bug: header sticky + tre card sovrapposte. Toggle isolation: isolate sul wrapper .pricing:
Quando un componente usa z-index al suo interno, prendete in considerazione isolation: isolate sul contenitore principale. Garantisce che la classifica interna non interferisca con il resto della pagina, senza aggiungere inquinamento di numeri globali. Non è obbligatoria in ogni caso — semplici stack verticali senza sovrapposizioni non la richiedono — ma è una buona prima mossa per componenti complessi. È l'approccio che usa anche Tailwind, con la classe isolate.
z-indexPrima di raggiungere per z-index, chiedetevi: posso semplicemente riordinare l'HTML?
Se due elementi sono entrambi posizionati e hanno z-index: auto, il browser li dipinge secondo l'ordine del DOM: l'elemento scritto dopo va sopra.
Versione "con z-index":
<div class="wrapper">
<div class="card">Hello World</div>
<span class="decoration decoration-a" aria-hidden="true"></span>
<span class="decoration decoration-b" aria-hidden="true"></span>
</div>
.wrapper { position: relative; }
.card { position: relative; z-index: 2; }
.decoration { position: absolute; z-index: 1; }
Versione "senza z-index" — basta mettere i blob prima della card nell'HTML:
<div class="wrapper">
<span class="decoration decoration-a" aria-hidden="true"></span>
<span class="decoration decoration-b" aria-hidden="true"></span>
<div class="card">Hello World</div> <!-- dopo nel DOM → sopra -->
</div>
.wrapper { position: relative; }
.card { position: relative; }
.decoration { position: absolute; }
Risultato identico, zero z-index da gestire.
z-indexCard sopra ai blob, grazie al layering esplicito.
Stesso risultato visivo, ma senza introdurre numeri globali.
Chi naviga con la tastiera incontra gli elementi nell'ordine del DOM. Spostare elementi decorativi prima del contenuto principale va bene: con Tab non ci si ferma sulle immagini decorative. Spostare elementi interattivi (link, bottoni, input) produce un flusso di tabulazione innaturale e penalizza chi usa la tastiera o uno screen reader.
Regola: usate il trucco del riordino DOM solo per elementi non interattivi (sfondi, decorazioni, ombre). Per elementi interattivi con problemi di sovrapposizione, usate z-index + isolation.
Aprite CodePen e incollate questo codice. Scorrete la pagina: noterete che, al passaggio delle card, l'header "si infila" sotto la card centrale. Il vostro compito è ripararlo senza toccare i z-index dell'header o delle card.
<header>Synergistic Inc.</header>
<main>
<section class="pricing">
<article class="card">Starter</article>
<article class="primary card">Pro (in evidenza)</article>
<article class="card">Enterprise</article>
</section>
<div class="filler"></div>
</main>
body { margin: 0; font-family: sans-serif; }
header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 60px;
background: hotpink;
color: white;
line-height: 60px;
text-align: center;
z-index: 2;
}
main { padding-top: 120px; }
.pricing {
/* Aggiungete qui la riga che ripara il bug */
display: flex;
gap: 16px;
padding: 32px;
}
.card {
position: relative;
z-index: 1;
background: white;
border: 2px solid #ddd;
padding: 24px;
flex: 1;
}
.primary.card {
z-index: 2;
margin: -24px 0;
background: #fffbe6;
border-color: gold;
}
.filler { height: 800px; }
isolation: isolate a .pricing. Rifate scroll: il bug è sparito?position: relative; z-index: 1 su .pricing. Funziona lo stesso?z-index della card centrale a 1. Il bug è risolto, ma a che costo visivo? (suggerimento: l'enfasi sulla card centrale)z-index, Stacking Context| Concetto | Spiegazione |
|---|---|
| Ordine di default | Gli elementi posizionati stanno sopra i non-posizionati; a parità di stato, vince l'ordine del DOM |
z-index |
Cambia l'ordine di sovrapposizione; accetta solo interi; default auto (non crea stacking context; z-index: 0 invece lo crea) |
Dove funziona z-index |
Positioned Layout, Flexbox, Grid. Non in Flow layout puro |
| Bridge con Grid | z-index sui grid children è un caso particolare della regola generale, non un'eccezione magica |
| Stacking context | Gruppo di elementi le cui z-index si confrontano fra loro. z-index non è una classifica globale |
| Cosa crea un context | position + z-index, position: fixed/sticky, opacity < 1, transform, isolation: isolate |
isolation: isolate |
Crea uno stacking context senza position e senza un numero. L'antidoto alle z-index wars |
| DOM-order swap | Trick per evitare z-index su elementi non interattivi. Mai sugli interattivi (tab order) |
| Euristica consigliata | Ogni componente con z-index interni → isolation: isolate sul wrapper (utile come prima mossa; stack verticali semplici non la richiedono) |
Prossima sezione: usciamo dal flusso in modi ancora più radicali. position: fixed, overflow, scroll container e il famigerato "transformed ancestor" che fa impazzire i developer.
fixed, overflow, Scroll ContainerCon relative e absolute abbiamo imparato a far galleggiare gli elementi dentro la pagina. Ora alziamo il tiro.
In questa sezione studiamo tre meccanismi che ridefiniscono il rapporto fra un elemento e il suo contenitore:
position: fixed: l'elemento ignora persino i suoi antenati e si aggancia al viewport.overflow: decidiamo cosa succede quando il contenuto sbrodola fuori dal box genitore.overflow attiva senza dircelo.Nella lezione di Grid abbiamo usato overflow come "medicina" per aggiustare una griglia che non collaborava. Qui lo smontiamo da zero: capiremo quali valori accetta, perché hidden e clip non sono la stessa cosa, e soprattutto cos'è uno scroll container — uno dei concetti più trappolosi di tutto CSS.
Alla fine della sezione: saprete diagnosticare quel bug classico che tutti incontriamo prima o poi: "ho messo position: fixed e non funziona".
position: fixed - Lo Stacco dal DOMposition: fixed è il cugino ribelle di position: absolute.
Fixed si può definire un "absolute sotto steroidi". Il meccanismo è simile: l'elemento esce dal flusso, ignora i fratelli, non occupa spazio. Ma il suo contenitore di riferimento è diverso.
position: absolute |
position: fixed |
|---|---|
| Si aggancia al primo antenato posizionato | Si aggancia sempre al viewport |
| Scrolla con la pagina | NON scrolla: resta fermo sullo schermo |
| Cerca un containing block nel DOM | Ignora il DOM, guarda l'"initial containing block" (il viewport) |
.help-btn {
position: fixed;
right: 32px;
bottom: 32px;
}
<main class="page">
<button class="help-btn">Help</button>
<p>Contenuto lungo della pagina...</p>
</main>
Questo bottone resta cementato nell'angolo basso-destro dello schermo, indipendentemente da quanto scrollate la pagina o da dove si trovi nel DOM.
Casi d'uso tipici: pulsanti di aiuto, chat widget, menu mobile aperto, banner di cookie, overlay modali.
E se togliamo gli anchor point (top, left, right, bottom)? L'elemento resta nella sua posizione naturale (come fosse absolute senza offset), ma mantiene comunque il comportamento fixed: non scrolla. È una curiosità utile per "ereditare" una posizione in-flow senza perdere l'ancoraggio al viewport.
Ricordate il trucco del centraggio visto nella Sezione 1? Funziona anche con position: fixed, e questo è il pattern più usato sul web per le modali.
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 85%;
height: 200px;
margin: auto;
}
<div class="modal" role="dialog" aria-modal="true">
<h3>Conferma</h3>
<p>Testo della modale</p>
</div>
Cosa succede:
0 dicono: "occupa tutto il viewport".width e height espliciti riducono l'elemento.margin: auto distribuisce equamente lo spazio residuo → centraggio perfetto, sia in orizzontale che in verticale.Il risultato: una modale centrata sullo schermo, che resta centrata anche se l'utente scrolla.
I quattro offset a 0 dilatano la box al viewport.
width/heightLe dimensioni esplicite riducono la scatola.
margin: automargin: auto distribuisce lo spazio residuo su tutti e quattro i lati.
Provate a sostituire position: fixed con position: absolute e poi scrollate. Vedrete la differenza: con absolute la modale si aggancia al primo antenato posizionato e scorre con la pagina; con fixed resta inchiodata al viewport. Per una modale vera, quasi sempre volete fixed.
Forward reference: Nell'esercizio finale della lezione costruirete esattamente questo tipo di overlay, combinandolo con z-index, overflow, header sticky e un tocco di transform.
Preparatevi, perché questo è uno dei gotcha più frustranti di CSS. Non è un bug: è il comportamento specificato — ma è quasi sempre una sorpresa. Vi capiterà di scrivere position: fixed e di vedere l'elemento che... scrolla con la pagina. Eppure il CSS è giusto!
.container {
/* Questa proprietà cambia il containing block per TUTTI i discendenti */
transform: translate(1px, 1px);
}
.fixed {
position: fixed;
top: 0;
}
<div class="container">
<div class="fixed">.fixed</div>
<div class="content">...contenuto lungo...</div>
</div>
.container ha davvero transform: translate(1px, 1px).
Scrollate questo frame: la pill rosa dovrebbe restare fissata al viewport, ma non lo fa.
Il motivo e' proprio il gotcha di questa slide: il transformed ancestor diventa il nuovo containing block.
Quindi .fixed si comporta come se fosse ancorata a .container, non al viewport del frame.
Paragrafo filler 1 per creare scroll.
Paragrafo filler 2 per creare scroll.
Paragrafo filler 3 per creare scroll.
Paragrafo filler 4 per creare scroll.
Se toglieste transform a .container, la pill rosa resterebbe agganciata al viewport del frame invece di scorrere via.
Cosa succede: quando un antenato (genitore, nonno, bis-bisnonno...) ha una transform, diventa lui il containing block sia per i figli fixed sia per i figli absolute. Per fixed l'effetto è dirompente: l'elemento smette di ancorarsi al viewport e inizia a comportarsi come absolute rispetto a quell'antenato, scrollando con il contenuto. Per absolute l'effetto è più sottile: l'elemento si annida all'antenato trasformato invece che al position: relative che vi aspettavate.
Perché esiste questa regola? Le transform compongono in un contesto 3D/compositor. CSS deve garantire che il layout dei discendenti rimanga coerente con la trasformazione applicata all'antenato, e perciò promuove quell'antenato a containing block.
transform (anche translate, rotate, scale, ecc. quando usati come proprietà singole)filterwill-change: transformperspectivebackdrop-filterIn un progetto grande potreste avere 15-20 antenati sopra al vostro elemento fixed. Dobbiamo controllarli tutti uno per uno? No: c'è uno snippet JavaScript che risale il DOM e vi dice chi è il colpevole.
// Replace ".the-fixed-child" for a CSS selector
// that matches the fixed-position element:
const selector = '.the-fixed-child';
function findCulprits(elem) {
if (!elem) {
throw new Error(
'Could not find element with that selector'
);
}
let parent = elem.parentElement;
while (parent) {
const {
transform,
willChange,
filter,
} = getComputedStyle(parent);
if (
transform !== 'none' ||
willChange === 'transform' ||
filter !== 'none'
) {
console.warn(
'Found a culprit!\n',
parent,
{ transform, willChange, filter }
);
}
parent = parent.parentElement;
}
}
findCulprits(document.querySelector(selector));
Regola pratica: se position: fixed non funziona, la prima cosa da guardare non è il CSS dell'elemento, ma i suoi antenati.
overflow: Gestire il Contenuto che non Sta nel BoxPassiamo al secondo tema della sezione: overflow.
Il problema: di solito i block element hanno altezza variabile e crescono per contenere i figli. Ma quando fissiamo height (o max-height), creiamo una condizione impossibile: "contieni tutto il contenuto in 100px".
Esempio classico: il nome completo di Pablo Picasso.
.info {
max-height: 100px;
border: 3px solid;
}
<div class="info">
<strong>Name:</strong> Pablo Diego José Francisco de Paula Juan
Nepomuceno María de los Remedios Cipriano de la Santísima Trinidad
Ruiz y Picasso
</div>
<div class="info">
<strong>Born:</strong> 25 October 1881
</div>
Cosa fa il browser di default? Lascia che il testo sbordi fuori dai confini, senza però considerarlo nei calcoli di layout. Risultato: il testo sovrascrive il box successivo. Un bel pasticcio.
Nella prossima slide vediamo come domare questo comportamento.
visible) di overflow| Valore | Comportamento | Quando usarlo |
|---|---|---|
visible |
Default: il contenuto sbrodola fuori, il layout lo ignora | Quando vogliamo che qualcosa esca apposta (badge, tooltip, trucchi con margin negativo) |
scroll |
Sempre barre di scroll, anche se non servono | Raramente (interfacce che vogliono uno spazio di scroll "riservato") |
auto |
Barre di scroll solo se servono | Il valore più utile. Il nostro default pratico |
hidden |
Il contenuto fuori viene tagliato via | Per trucchi grafici, decorazioni, effetti "guarda-non-tocca" |
clip |
Come hidden, ma non crea uno scroll container |
Quando volete solo ritagliare senza effetti collaterali |
Nota: overflow è uno shorthand per overflow-x e overflow-y. Potete gestire gli assi separatamente:
.wrapper {
overflow-y: auto; /* scroll verticale se serve */
overflow-x: hidden; /* taglia l'eventuale overflow orizzontale */
}
<div class="wrapper">
<p>Contenuto molto piu lungo dell'altezza disponibile...</p>
</div>
visibleRiga 1
Riga 2
Riga 3
Riga 4
Riga 5
Il contenuto esce dal box e può invadere quello sotto.
autoRiga 1
Riga 2
Riga 3
Riga 4
Riga 5
Scorre solo quando serve.
hiddenRiga 1
Riga 2
Riga 3
Riga 4
Riga 5
Taglia ma crea comunque uno scroll container.
clipRiga 1
Riga 2
Riga 3
Riga 4
Riga 5
Ritaglia senza introdurre scroll programmatico.
Regola operativa: usate overflow: auto quando pensate che un elemento possa sbordare. Usate overflow: hidden quando volete nascondere apposta il contenuto che esce. Non mischiate i due intenti.
A prima vista overflow: hidden e overflow: scroll sembrano opposti: uno nasconde, l'altro fa scrollare. In realtà, fanno la stessa cosa di base con una differenza nascosta.
Il segreto: overflow: hidden è overflow: scroll con le barre disabilitate.
.wrapper {
overflow: hidden;
height: 100px;
}
<div class="wrapper">
<ol>
<li><a href="/">Link uno</a></li>
<li><a href="/">Link due</a></li>
...
<li><a href="/">Link sei</a></li>
</ol>
</div>
Provate a premere Tab per muovervi fra i link: quando il focus arriva su un link "nascosto", il container scrolla da solo per mostrarlo! Eppure non c'era nessuna barra di scroll visibile.
/*
overflow: hidden -> è uno scroll container senza UI
overflow: scroll -> è uno scroll container con UI sempre visibile
overflow: auto -> è uno scroll container con UI condizionale
*/
overflow: hiddenNon vedete le barre, ma il contenitore può comunque scorrere via focus o script.
overflow: scrollStesso meccanismo, ma con UI sempre visibile.
Perché è importante? Perché introduce un concetto nascosto che confonde mezzo mondo del web: lo scroll container. Lo vediamo ora.
Immaginate una valigia con il fondo finto: all'esterno sembra chiusa di quella dimensione, ma dentro c'è molto più spazio — bisogna "frugare" (scrollare) per trovare tutto quello che contiene. Per chi conosce Doctor Who: è la TARDIS — una cabina telefonica fuori, enormità dentro. Entrambe le metafore dicono la stessa cosa: il container sembra piccolo, ma il suo contenuto interno è molto più grande.
.wrapper {
height: 150px;
overflow-y: auto;
}
.photo {
width: 100%;
}
<div class="wrapper">
<img class="photo" src="tall-image.jpg" alt="Foto molto alta">
</div>
Il .wrapper è alto 150px, ma può contenere una foto altissima: è un portale verso una dimensione alternativa.
overflow a scroll, auto, hidden (o clip su un solo asse in alcuni casi).overflow-x: hidden; overflow-y: auto) ma richiede configurazione esplicita ed è facile sbagliare, perché il browser promuove comunque il container a scroll container su entrambi gli assi. Se attivate overflow-x, il container diventa uno scroll container anche sull'asse Y.Perché questo è un problema? Perché spesso vogliamo una cosa sola: nascondere l'overflow orizzontale, ma lasciar sbrodolare quello verticale (per esempio, per far uscire dei pallini decorativi dall'alto). Di solito non funziona come ci si aspetta.
.wrapper {
overflow-x: hidden; /* vogliamo solo questo */
overflow-y: visible; /* ...ma questo non funziona! */
}
Il browser lo ignora e tratta comunque tutto come scroll container.
La soluzione moderna: overflow: clip, che non crea uno scroll container.
.wrapper { overflow-x: clip; } /* questo funziona davvero */
clipoverflow: clip ha un rovescio: non ha le guardrail di hidden. Con hidden, i link e i bottoni ritagliati restano raggiungibili via Tab (il container scrolla automaticamente). Con clip, quei link diventano invisibili e inaccessibili. Usate clip per contenuto decorativo; hidden quando ci sono elementi interattivi dentro.
overflow: hidden vs overflow: clip — Come si Comportano con la TastieraMini-pratica da 3 minuti. Aprite CodePen e verificate sul posto la differenza tra i due valori di clipping, e cosa succede quando premete Tab su un bottone nascosto.
<div class="container">
<div class="inner">
<button class="btn">Visibile</button>
<button class="btn">Nascosto (ma raggiungibile?)</button>
</div>
</div>
.container {
width: 200px;
height: 60px;
border: 2px solid steelblue;
/* Provate: overflow: hidden; */
/* Provate: overflow: clip; */
overflow: hidden;
}
.inner {
display: flex;
gap: 8px;
padding: 8px;
}
.btn {
flex-shrink: 0;
padding: 8px 16px;
background: deeppink;
color: white;
border: none;
cursor: pointer;
}
overflow: hidden: premete Tab dal primo bottone. Il secondo bottone è nascosto visivamente, ma il browser scrolla automaticamente il container per portarlo in vista. Il container è uno scroll container — lo scroll avviene via programma, non con la barra.overflow: clip: premete Tab dal primo bottone. Il secondo bottone non appare: clip non crea uno scroll container, quindi il browser non può scrollare automaticamente. Il bottone esiste nel DOM ma è inaccessibile.clip solo per contenuto decorativo. Se dentro ci sono elementi interattivi (bottoni, link, input), usate hidden oppure ripensate il layout.Finora abbiamo parlato di overflow verticale. Ma il pattern più comune di overflow orizzontale lo conoscete tutti: la tab bar orizzontale scrollabile che trovate su quasi ogni app mobile (iOS, Android, YouTube, Amazon…).
Il problema: i tab sono elementi inline di default. Come le parole di un paragrafo, vanno a capo quando finisce lo spazio. Il container non sborda mai, e lo scroll non si attiva.
.tab-bar {
overflow-x: auto; /* attiva lo scroll orizzontale */
white-space: nowrap; /* impedisce l'a capo dei tab */
display: flex; /* alternativa moderna a white-space: nowrap */
}
.tab {
flex-shrink: 0; /* i tab non si restringono */
padding: 12px 20px;
}
<nav class="tab-bar">
<a class="tab" href="#">Overview</a>
<a class="tab" href="#">Dettagli</a>
<a class="tab" href="#">Recensioni</a>
<a class="tab" href="#">FAQ</a>
<a class="tab" href="#">Prezzi</a>
</nav>
Perché white-space: nowrap? Dice al container: "tieni tutto sulla stessa riga, non andare mai a capo". Con display: flex + flex-shrink: 0 si ottiene lo stesso risultato in modo più esplicito e moderno.
Con overflow-x: auto si attiva lo scroll orizzontale. Ma i tab si comportano come parole di testo e vanno a capo. white-space: nowrap (o flex + flex-shrink: 0) risolve proprio questo: forza i figli a stare tutti sulla stessa riga, creando il debordamento che overflow-x: auto gestirà.
Nota su nowrap: è scritto tutto attaccato, non no-wrap. Non c'è una buona ragione — il CSSWG lo ha classificato come un errore storico del linguaggio.
fixed SabotatoAprite CodePen e incollate il codice. Avete un header che dovrebbe restare fissato in alto durante lo scroll. Ma non funziona. Missione: scoprite perché e sistematelo, modificando solo una proprietà CSS.
<div class="page-wrapper">
<header class="header">Header che dovrebbe stare fermo</header>
<section class="hero">Contenuto della pagina</section>
<p>Scrollate in basso per vedere cosa succede…</p>
</div>
body {
height: 300vh;
margin: 0;
}
.page-wrapper {
/* Qui c'è il colpevole - che fa? */
filter: grayscale(0);
padding: 20px;
}
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
background: deeppink;
padding: 16px;
color: white;
}
.hero {
height: 400px;
background: #eee;
padding: 40px;
}
position. È davvero fixed?.page-wrapper e cercate proprietà sospette (transform, filter, will-change, perspective). Quale di queste, anche con valore "innocuo", rompe fixed?fixed.filter: grayscale(0) ma spostate l'header fuori da .page-wrapper nell'HTML. Funziona lo stesso. Perché? (Indizio: il figlio deve essere discendente del colpevole).overflow: hidden a .page-wrapper senza filter. Il fixed si rompe? No. overflow non rompe fixed (ma rompe sticky — lo vediamo nella prossima sezione).| Concetto | Spiegazione |
|---|---|
position: fixed |
Esce dal flusso come absolute, ma si aggancia al viewport, non al primo antenato posizionato |
| Transformed ancestor | Un antenato con transform/filter/will-change/perspective ruba il containing block a un figlio fixed |
overflow: visible |
Default. Il contenuto sbrodola fuori, il layout lo ignora |
overflow: scroll/auto/hidden |
Tutti e tre creano uno scroll container |
overflow: clip |
Ritaglia senza creare uno scroll container (moderno, ma perde i guardrail di accessibilità) |
| Scroll container | Meccanismo nascosto: gestisce sempre entrambi gli assi insieme. I figli non escono dai suoi bordi |
overflow-x + overflow-y: visible |
Non funziona: attivare un solo asse crea comunque scroll container |
white-space: nowrap |
L'ingrediente che, insieme a overflow: auto, permette lo scroll orizzontale |
absolute + overflow parent |
Il figlio viene "visto" dal parent solo se il parent è posizionato |
fixed + overflow parent |
Il figlio viene sempre ignorato dagli overflow degli antenati |
Regola d'oro di questa sezione: se position: fixed "non funziona", cercate un antenato con transform, filter, o perspective. Quasi sempre il colpevole è lì.
Prossima sezione: Sticky, il più subdolo dei valori di position. E poi un tema apparentemente semplice — "come nascondo un elemento?" — che nasconde più di quanto sembri.
sticky e l'Arte di NascondereIn questa sezione chiudiamo il cerchio su position con il quarto valore — sticky — e poi affrontiamo un tema apparentemente banale: come si nasconde un elemento? (Spoiler: ci sono almeno cinque modi diversi, e scegliere quello sbagliato può rendere il sito inaccessibile.)
Cosa vedremo:
position: sticky: il valore che scorre con la pagina e poi si blocca — come un ascensore che si ferma a un pianodisplay: none, visibility: hidden, opacity: 0, visually-hidden, aria-hidden, inertNella lezione di Grid abbiamo risolto un problema sticky con una ricetta (wrapper interno + align-self: start) senza spiegarla davvero. Oggi finalmente capiremo perché quel trucco serviva, e non ci sembrerà più magia.
position: sticky — Metà relative, metà fixedposition: sticky è il valore più recente di position. L'idea è semplice da enunciare, subdola da capire:
Pensatelo come un ascensore in cabina: si muove normalmente con la pagina (scorre insieme al contenuto, come relative). Quando il bordo superiore del viewport raggiunge il suo offset top, l'ascensore si blocca a quel piano — resta fisso lì — finché la cabina (il suo container) non lo porta fuori dalla vista.
Come mnemonica alternativa: è come un magnete attratto dal bordo del viewport, ma con raggio limitato — non si stacca mai dal suo container.
Mentre scrollate, l'elemento si comporta come relative. Arrivato al bordo, "si attacca" e inizia a comportarsi come fixed finché il suo genitore non sparisce dalla vista.
header {
position: sticky;
top: 0; /* ⚠️ Serve SEMPRE un offset! */
}
Due cose fondamentali:
top, left, right o bottom). Senza, sticky non fa nulla. L'offset non sposta l'elemento come in relative: dice al browser "a quale distanza dal bordo inizi a incollarti".Il pattern classico (header di sezione):
section h2 {
position: sticky;
top: 0;
}
<section>
<h2>Sezione 1</h2>
<p>Lorem ipsum...</p>
</section>
<section>
<h2>Sezione 2</h2>
<p>...</p>
</section>
Paragrafo uno: scrollate per vedere l'header incollarsi al bordo superiore.
Paragrafo due di riempimento per avere contenuto scrollabile dentro la sezione.
Paragrafo tre: fino a qui l'header rimane appiccicato.
Paragrafo quattro: il nuovo header spinge fuori il precedente.
Paragrafo cinque di filler per allungare la sezione.
Paragrafo sei, ancora filler.
Paragrafo sette.
Paragrafo otto di chiusura.
Ogni h2 si "incolla" in cima mentre scrolliamo dentro la sua sezione, e viene rimpiazzato dall'h2 successivo quando entra nella propria sezione. Magia? No: ogni header è semplicemente prigioniero del suo genitore.
Ricordate: absolute e fixed sono fantasmi. Non occupano spazio nel layout, i fratelli si comportano come se non esistessero.
sticky è diverso: come relative e static, è in-flow. Occupa spazio reale, e quello spazio resta occupato anche quando l'elemento è "incollato" e visivamente fermo nel viewport.
.main-box {
position: sticky; /* provate a cambiare in 'fixed' */
top: 0;
}
<section class="container">
<header class="main-box">Header</header>
<p>Contenuto...</p>
</section>
stickyL'header continua a occupare spazio nel layout.
fixedIl contenuto risale: l'header non riserva piu spazio.
Cambiando da sticky a fixed:
Tornando a sticky:
Se aggiungete position: sticky a un elemento esistente, non rompete il layout intorno. Potete trasformare un header normale in sticky senza toccare nient'altro. fixed invece vi costringe a compensare lo spazio che sparisce. È uno dei motivi per cui sticky è così comodo.
Curiosità: sticky funziona anche in orizzontale (left: 0), anche se è molto più raro. E funziona finalmente anche con gli elementi <thead>/<tbody> delle tabelle dal 2021.
overflow Rompe lo StickyEcco il motivo n°1 per cui sticky "non funziona" nei progetti reali.
main {
overflow: auto; /* il colpevole invisibile */
max-height: 200px;
}
header {
position: sticky;
top: 0;
}
<main>
<header class="mio-sticky">Header sticky</header>
<p>...contenuto lungo...</p>
</main>
Il principio: sticky si attacca al primo scroll container che incontra risalendo il DOM. Se un antenato ha overflow: auto/scroll/hidden, quello diventa il contesto di sticky, non più il viewport.
Conseguenza: l'header si incolla dentro quel container, non sulla pagina. Se il container non scrolla (perché non è abbastanza grande), sticky sembra semplicemente non fare nulla.
Perché è così subdolo: il colpevole può essere un bisnonno nel DOM, messo lì mesi prima per risolvere un problema di scrollbar orizzontale. E non c'è nessun errore in console.
Paragrafo uno: con overflow: auto sull'antenato, lo sticky si attacca al bordo del frame, non alla pagina.
Paragrafo due: filler.
Paragrafo tre: filler.
Paragrafo quattro: filler.
Paragrafo cinque: filler.
Paragrafo sei di chiusura.
Come si diagnostica:
// Da incollare in console.
// Risale il DOM e logga ogni antenato con overflow diverso da visible.
const selector = '.mio-sticky';
function findCulprits(elem) {
let parent = elem.parentElement;
while (parent) {
const { overflow } = getComputedStyle(parent);
if (['auto', 'scroll', 'hidden'].includes(overflow)) {
console.log(overflow, parent);
}
parent = parent.parentElement;
}
}
findCulprits(document.querySelector(selector));
Regola d'oro: se sticky non si attacca, prima di tutto controllate gli overflow degli antenati.
| # | Sintomo | Causa | Fix |
|---|---|---|---|
| #2 | Sticky non si attacca mai, anche senza overflow | Il container è più piccolo o alto come lo sticky: non c'è spazio di scroll entro cui "viaggiare" | Fate in modo che il container abbia più contenuto dello sticky stesso |
| #3 | Sticky è stirato come i fratelli in un Grid/Flex, e non si muove | In Grid/Flex gli item sono stretchati sull'asse trasversale. Uno sticky stirato non ha spazio di manovra nel genitore | Avvolgetelo in un wrapper e usate align-self: start |
| #4 | C'è un sottile gap di 1px tra lo sticky e il bordo del viewport | Bug di arrotondamento fra pixel frazionari in Chrome | top: -1px; invece di 0 |
Ricordate il trucco che abbiamo usato nella lezione precedente? Un wrapper interno intorno allo sticky con align-self: start sul wrapper? Ecco perché serviva. Un grid item è stretchato di default sulla riga: lo sticky non può muoversi perché è già alto quanto la cella. Il wrapper funge da "cella più alta", lo sticky dentro è libero. Non era magia: era il Bug #3 in versione Grid.
/* Il pattern di Grid ricordato */
.sidebar-wrapper {
align-self: start; /* la cella smette di stretchare */
}
.sidebar {
position: sticky;
top: 0;
}
<div class="layout">
<div class="sidebar-wrapper">
<aside class="sidebar">Indice</aside>
</div>
<main>Contenuto lungo...</main>
</div>
Paragrafo uno: cambiate scenario dal toggle per vedere ciascun bug in azione.
Paragrafo due: filler.
Paragrafo tre: filler.
Paragrafo quattro: filler di chiusura.
Regola pratica (tutti e 4 i bug): sticky richiede spazio, containing block giusto, e nessun scroll container ribelle. Se manca uno dei tre, fallisce in silenzio.
Aprite CodePen e incollate il codice. Ci sono tre scenari sticky che non funzionano. La missione: fate in modo che l'header pink si attacchi in cima quando scrollate, cambiando solo il CSS (non l'HTML — lo starter è già completo).
<div class="scenario scenario-1">
<header class="sticky-header">Sticky #1</header>
<p>Testo lungo... (ripetete per avere contenuto scrollabile)</p>
</div>
<div class="scenario scenario-2">
<header class="sticky-header">Sticky #2</header>
</div>
<div class="scenario scenario-3">
<div class="sidebar-wrapper">
<aside class="sticky-header">Sticky #3 (sidebar)</aside>
</div>
<main>Contenuto lungo...</main>
</div>
body { margin: 0; font-family: sans-serif; }
.scenario {
border: 3px solid;
margin: 40px 0;
padding: 16px;
}
.sticky-header {
position: sticky;
top: 0;
background: deeppink;
color: white;
padding: 12px;
}
/* --- Scenario 1: parent con overflow --- */
.scenario-1 {
overflow: auto;
max-height: 300px;
}
/* --- Scenario 2: container troppo corto --- */
.scenario-2 {
/* L'header è alto quanto il container: non c'è spazio */
}
/* --- Scenario 3: sticky in una grid cell --- */
.scenario-3 {
display: grid;
grid-template-columns: 200px 1fr;
gap: 16px;
}
overflow: auto e max-height: torna a funzionare normalmente.align-self: start al .sidebar-wrapper per vedere sticky attivarsi e disattivarsi. Senza align-self: start, la sidebar è stretchata su tutta la riga e non ha spazio di manovra.top: 0 in top: 20px. Lo sticky si ferma 20px sotto il bordo, non attaccato.sticky? I browser moderni vi segnalano anche il "contenitore di attivazione" di sticky.Ultima grande area di questa lezione: come si nasconde qualcosa in CSS?
Sembra banale, ma c'è molta sottigliezza, e scegliere il metodo sbagliato significa creare problemi di accessibilità o rompere la SEO.
| Metodo | Visibile? | Occupa spazio? | Cliccabile/focusabile? | Letto da screen reader? |
|---|---|---|---|---|
display: none |
No | No | No | No |
visibility: hidden |
No | Sì (lascia il buco) | No | No |
opacity: 0 |
No | Sì | Sì! ⚠️ | Sì! ⚠️ |
.visually-hidden (classe) |
No | No (quasi) | Sì se elemento focusable | Sì (apposta) |
aria-hidden="true" |
Sì (!) | Sì | Sì (!) | No |
display: none — il classico. L'elemento svanisce dal layout come se non esistesse. Utile per toggle responsive:
.desktop-header { display: none; }
@media (min-width: 1024px) {
.desktop-header { display: block; }
.mobile-header { display: none; }
}
visibility: hidden — mantello dell'invisibilità. L'elemento sparisce ma tiene il posto, come un fantasma. Utile per rivelare risposte al passaggio del mouse, o per riservare spazio a contenuto che arriverà:
.answer { visibility: hidden; }
.answer-wrapper:hover .answer { visibility: visible; }
<div class="row">
<div class="item a">A</div>
<div class="item b">B</div>
<div class="item c">C</div>
</div>
display: noneIl layout si richiude: lo spazio di B sparisce.
visibility: hiddenB non si vede, ma il suo buco resta li.
opacity: 0B esiste ancora: occupa spazio e può restare attivo.
Curiosità: visibility: hidden può essere selettivamente annullata sui figli. Se il padre è hidden, un figlio specifico può essere visibility: visible. Nessun altro metodo lo permette.
Nella prossima slide vediamo gli altri tre, e perché due di essi sono trappole di accessibilità.
opacity, Visually-Hidden, aria-hidden, inertopacity: 0 — l'elemento diventa trasparente, ma non è nascosto davvero:
.flourish { opacity: 0.5; } /* semi-trasparente */
.flourish { opacity: 0; } /* invisibile... ma ancora lì */
L'utente con tastiera può ancora "tabbare" sui bottoni invisibili senza vedere dove sta il focus. Gli screen reader li annunciano comunque. Non usate opacity: 0 per nascondere! Usatelo per animazioni di fade, dove volete passare gradualmente da visibile a invisibile (e a quel punto aggiungete anche visibility: hidden o pointer-events: none a fine animazione).
Visually-Hidden — la tecnica corretta per "nascondi visivamente, tieni per screen reader":
.visually-hidden {
clip-path: inset(50%);
height: 1px;
width: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
}
(Il vecchio snippet usava clip: rect(0 0 0 0), che è deprecato — ancora funzionante ma sconsigliato. clip-path: inset(50%) è supportato ovunque. white-space: nowrap evita che il testo vada a capo nei contenitori inline.)
<button>
<span class="visually-hidden">Apri le impostazioni</span>
<svg><!-- icona ingranaggio --></svg>
</button>
L'utente vedente vede l'icona, lo screen reader legge "Apri le impostazioni". Spesso preferibile all'attributo aria-label quando il testo è contenuto: i servizi di traduzione automatica leggono il testo visibile ma non gli attributi aria-label. Non è una regola assoluta — aria-label resta valido per bottoni con icona singola, input, o controlli dove un nodo di testo non starebbe bene.
aria-hidden="true" — il contrario di visually-hidden: visibile agli occhi, invisibile agli screen reader:
<a href="/">
Go Home
<span aria-hidden="true">Go Home</span> <!-- duplicato decorativo per animazione -->
</a>
Lo screen reader dirà "Go Home" una volta sola invece di due.
aria-hidden non rimuove gli elementi dall'ordine di Tab! Se dentro c'è un link, un utente con tastiera può ancora raggiungerlo — senza però sentirlo annunciato. Situazione pessima.
inert — l'attributo moderno (2023) che risolve il problema:
<!-- ✅ Corretto: inert da solo basta -->
<p inert>
This paragraph contains <a href="/">a link</a>.
</p>
<!-- ⚠️ Didattico (per confronto, non il pattern raccomandato) -->
<p inert aria-hidden="true">...</p>
inert rimuove l'elemento e tutti i suoi discendenti dall'interazione: niente focus, niente click, niente screen reader. Supportato da tutti i browser moderni; è il modo giusto per disattivare completamente una sezione (es. contenuto dietro una modale aperta).
opacity: 0Il secondo bottone non si vede, ma continua a esistere.
.visually-hiddenVoi vedete solo l'icona; il testo resta disponibile per gli screen reader.
inertContenuto dietro la modale
Link disattivatoIl blocco resta visibile, ma e trattato come non interattivo.
Aggiungere aria-hidden="true" insieme a inert è ridondante e in alcuni screen reader introduce un doppio filtraggio. inert già rimuove l'elemento dall'albero di accessibilità: non occorre aria-hidden. Usate aria-hidden="true" soltanto su decorazioni visibili (icone, duplicati decorativi) che non devono essere annunciate, ma senza inert.
| Concetto | Spiegazione |
|---|---|
position: sticky |
Come un ascensore: scorre con la pagina (relative), poi si blocca al bordo (fixed) finché il parent è in vista |
| Offset obbligatorio | Senza top/left/right/bottom, sticky non fa nulla |
| In-flow | Occupa spazio reale, a differenza di absolute/fixed |
| Prigioniero del parent | Non scrolla mai fuori dal suo container |
| Scroll container ancestrale | Se un antenato ha overflow, quello diventa il contesto (bug #1) |
| Ricetta anti-stretch | In Flex/Grid: wrapper esterno + align-self: start |
| Metodo | Usatelo quando… |
|---|---|
display: none |
Volete togliere completamente dal layout (toggle responsive, contenuto condizionale) |
visibility: hidden |
Volete nascondere ma mantenere lo spazio (reveal al hover) |
opacity: 0 |
Solo per animazioni di fade, mai come metodo di hiding permanente |
.visually-hidden |
Volete nascondere agli occhi ma mantenere per screen reader (icone senza label visibile) |
aria-hidden="true" |
Volete che gli screen reader ignorino un elemento visibile (duplicati decorativi) |
inert |
Volete disattivare tutto di un sottoalbero (background di una modale aperta) |
Regola d'oro accessibilità: prima di nascondere qualcosa, chiedetevi "per chi lo voglio nascondere: la vista, la tastiera, lo screen reader, o tutti?". La risposta determina il metodo.
Prossima sezione: il bonus della lezione. Le transform, lo strumento per spostare, scalare e ruotare gli elementi senza toccare il layout. Il complemento perfetto di tutto quello che abbiamo visto finora.
transformSiamo all'ultima sezione. Tutto quello che abbiamo visto finora (position, z-index, overflow, sticky) serve a decidere dove un elemento sta nel layout.
Le transform fanno una cosa diversa: decidono come appare. Sono uno strumento che ci permette di:
translate)scale)rotate)skew)Tutto senza toccare il layout. I fratelli non si spostano, il genitore non cambia dimensione, gli algoritmi di Flow/Flex/Grid non se ne accorgono.
Perché sono nella lezione di positioning? Perché:
top/left — entrambi spostano elementi "sopra al layout" senza riflusso.translate(-50%, -50%)) si usa insieme a position: fixed.Le transform sono fondamentali anche per le animazioni performanti. Il browser può applicarle sulla GPU senza rifare il calcolo di layout e paint. Ma di animazioni parleremo nel Modulo 8 — oggi ci concentriamo sui meccanismi statici.
transform: Trattare l'Elemento Come Fosse un'ImmaginePrima di tuffarci nelle funzioni, un mental model che risolve metà dei dubbi futuri.
Le transform trattano l'elemento come una texture appiattita. Come se prendessero una screenshot dell'elemento + figli, la incollassero in un livello separato, e poi la deformassero in Photoshop.
Conseguenze pratiche:
.box {
/* Forma generale */
transform: <funzione>(<valore>);
}
/* Esempi */
.b1 { transform: translateX(20px); }
.b2 { transform: scale(1.5); }
.b3 { transform: rotate(45deg); }
.b4 { transform: skewX(10deg); }
<div class="box">box</div>
translatescalerotateskewLe transform si applicano come stringa. Possiamo passarne una o più insieme (ci torniamo nella slide sulla composizione).
Perché permette al browser di saltare gli step di layout e paint durante le animazioni. Ricalcolare width significa ri-eseguire tutto l'algoritmo di layout e il line-wrapping. Le transform sono spesso molto più performanti perché il browser può ottimizzarle senza ricalcolare layout e line-wrapping — in molti casi spostando direttamente la texture già composita. Vi ricordate quante cose abbiamo fatto in questa lezione per spostare elementi senza riflusso? Le transform lo fanno gratis, con un bonus di performance.
translate(): Muovere Elementi in X e Ytranslate sposta un elemento lungo X e Y. Valori positivi → destra/giù; negativi → sinistra/su.
.box { transform: translateY(20px); }
/* Equivalente a: */
.box { transform: translate(0px, 20px); }
Possiamo anche usare le versioni mono-asse:
.box { transform: translateX(50px); }
.box { transform: translateY(-30px); }
<div class="box">box</div>
.box {
transform: translate(0px, 0px);
}
La proprietà più potente di translate: le percentuali sono relative all'elemento stesso, non al contenitore.
.box {
transform: translateY(-100%);
}
Questo sposta l'elemento verso l'alto della sua esatta altezza, qualunque essa sia. Se il box è alto 40px, si sposta di 40px; se è alto 200px, di 200px.
Perché è unico? In CSS, le percentuali sono quasi sempre relative al genitore:
| Proprietà | Percentuali rispetto a… |
|---|---|
width: 50% | Larghezza del genitore |
height: 50% | Altezza del genitore |
margin-left: 50% | Larghezza del genitore |
top: 50% | Altezza del genitore |
translate: 50% | Dimensioni dell'elemento stesso |
Questa differenza è l'arma segreta per il centraggio moderno, i modal, e una marea di trucchi che vediamo nelle prossime slide.
top/left o con translate()?Ora che abbiamo visto entrambi, è il momento di confrontarli. Non sono intercambiabili.
top/leftRiferimento: il containing block. Percentuali = dimensioni del genitore.
translate()Riferimento: l'elemento stesso. Percentuali = dimensioni del box.
| Caratteristica | top/left (positioned) |
transform: translate() |
|---|---|---|
| Sistema di riferimento | La posizione in-flow / il containing block | L'elemento stesso |
| Percentuali relative a… | Il genitore | L'elemento stesso |
| Riflow dei fratelli | No (se relative o absolute) |
No |
Richiede position settato? |
Sì (relative/absolute/fixed) |
No, funziona su qualsiasi elemento (tranne inline in flow) |
| Crea stacking context? | Sì (con z-index) |
Sì, sempre |
| Performance animazioni | Scarsa (ri-layout) | Ottima (GPU, niente layout) |
Crea containing block per fixed? |
No | Sì, trappola |
Quando usare top/left:
position: relativeabsoluteQuando usare translate():
-50% (vedi slide successiva)100%)rotate/scaleRegola pratica: se state per scrivere un'animazione, usate translate. Se state posizionando un layout statico con offset piccoli, top/left vanno benissimo.
Abbiamo visto due pattern di centraggio: il trucco con quattro 0 + margin: auto, e Flexbox/Grid con place-items: center. Ne esiste un terzo, il più usato per i modali e tooltip:
Provate a predire: cosa succede se applicate solo top: 50%; left: 50% a una modale, senza translate(-50%, -50%)? Dove finisce l'elemento? Perché il centro visivo risulta spostato in basso a destra? Prendete un secondo per ragionarci, poi leggete la spiegazione qui sotto.
.modal {
position: fixed; /* o absolute */
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
<div class="modal">Modale</div>
.modal {
position: absolute;
top: 50%;
left: 50%;
transform: translate(0%, 0%);
}
Cosa succede passo-passo:
top: 50% sposta il bordo superiore dell'elemento al 50% del viewport. L'elemento è troppo in basso.left: 50% fa lo stesso sull'asse X. L'elemento è troppo a destra.translate(-50%, -50%) sposta l'elemento della sua stessa metà all'indietro. Risultato: il centro dell'elemento è al 50%/50% del viewport.Perché non si fa solo con il margin auto? Perché margin: auto richiede width e height espliciti. Questo pattern funziona anche con elementi di dimensione sconosciuta (contenuto dinamico, modali che si ridimensionano con il testo).
/* Elemento con contenuto variabile */
.tooltip {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
/* niente width/height: si adatta al contenuto */
}
Questo è il caso in cui translate con percentuali fa qualcosa che nessun altro meccanismo CSS sa fare: usare la dimensione dell'elemento stesso come riferimento.
transform crea sempre uno stacking context. Quindi l'elemento centrato passa automaticamente "sopra" a fratelli non posizionati, anche senza z-index. Se avete bisogno di maggior controllo, continuate a usare z-index sui positioned layout.
scale(): Zoom Senza Effortscale cambia la dimensione di un elemento, proporzionalmente o per asse:
.box { transform: scale(1.5); } /* 1.5x sia X che Y */
.box { transform: scale(2, 0.5); } /* doppia larghezza, metà altezza */
.box { transform: scaleX(1.5); } /* solo asse X */
.box { transform: scaleY(0.8); } /* solo asse Y */
<div class="box">Ciao!</div>
Il valore è un moltiplicatore senza unità (come line-height): 2 = doppio, 0.5 = metà, 1 = invariato, 0 = invisibile.
.box {
transform: scale(1, 1);
}
A prima vista sembra equivalente a cambiare width/height, ma ha una differenza cruciale:
scale(1.5) -> il box E il testo dentro diventano 1.5x
width: 150% -> il box si allarga, il testo NON cambia (si ricalcola il wrap)
Le transform appiattiscono l'elemento come texture (ricordate la Slide 46). Il testo dentro scala visivamente, la dimensione dei caratteri effettiva non cambia. Non può essere diversamente: sarebbe un ri-layout.
A volte questo "difetto" è una feature:
Le librerie come Motion (ex Framer Motion) sfruttano scale per animazioni super-performanti. Per animazioni che non devono deformare il testo usano una tecnica avanzata chiamata "inverse scale" sui figli, fuori dallo scope della lezione. L'importante è che abbiate il modello giusto: scale deforma tutto.
rotate() e skew() — Le Funzioni "Espressive"rotate ruota l'elemento attorno al proprio centro:
.box { transform: rotate(45deg); } /* 45 gradi orari */
.box { transform: rotate(-90deg); } /* 90 antiorari */
.box { transform: rotate(0.25turn); } /* = 90deg */
.box { transform: rotate(1turn); } /* giro completo (360deg) */
<div class="box">box</div>
L'unità più comune è deg (degrees). L'unità turn è comodissima: 1turn = 360°, 0.5turn = 180°. Funziona fin da Internet Explorer 9.
.box {
transform: rotate(0deg) skewX(0deg);
}
skew inclina l'elemento sul piano 2D, come se lo schiacciassimo di lato:
.box { transform: skewX(20deg); }
.box { transform: skewY(10deg); }
.box { transform: skew(15deg, 5deg); } /* shorthand */
skew — usato di rado su contenuto vero (il testo diventa fastidioso da leggere), ma buono da riconoscere. Molto comune su elementi decorativi diagonali — le "bande" oblique tra sezioni delle landing page. Si può inclinare il box lasciando il testo dritto applicando un contro-skew sul figlio.
.diagonal-banner {
background: linear-gradient(90deg, #ff006e, #8338ec);
transform: skewY(-3deg);
padding: 40px 0;
}
.diagonal-banner > * {
transform: skewY(3deg); /* annulla l'inclinazione per il contenuto interno */
}
transform-origin: Il Centro della Rotazione (e dello Scale)Di default, ogni transform pivota attorno al centro dell'elemento. Possiamo cambiarlo con transform-origin:
.box {
transform-origin: top left; /* pivot in alto a sinistra */
transform: rotate(30deg);
}
Valori possibili:
transform-origin: center; /* default */
transform-origin: top left;
transform-origin: 50% 100%; /* centro-basso */
transform-origin: 0 0; /* equivalente a top left */
transform-origin: 20px 40px; /* pixel espliciti */
<div class="box">box</div>
.box {
transform-origin: center;
transform: rotate(0deg);
}
Effetto visibile:
center → l'elemento gira in place.bottom → l'elemento "dondola" sul piede.top right → l'elemento "cade" come una porta.top left → l'elemento cresce verso basso-destra, non in tutte le direzioni.Caso d'uso classico: animazioni di "growing from" (un menu a tendina che scende dal suo bottone):
.dropdown {
transform-origin: top center;
transform: scaleY(0); /* invisibile all'inizio */
transition: transform 200ms;
}
.dropdown.open {
transform: scaleY(1); /* cresce verso il basso */
}
Con origin center il menu sembrerebbe uscire dal nulla al centro — brutto. Con origin top center cresce naturalmente verso il basso dal bottone sopra di lui.
Possiamo applicare più transform insieme, separate da spazi:
.box {
transform: rotate(45deg) translateX(100px);
}
Mini-quiz — prima di leggere: disegnate su un foglio dove finisce una box rossa di 100×100px dopo rotate(45deg) translateX(100px), partendo dal centro dello schermo. Poi confrontate con il risultato qui sotto.
Il modello mentale corretto: il CSS legge la lista delle funzioni da sinistra a destra, come uno stack di istruzioni su un sistema di riferimento mobile. Ogni funzione modifica il sistema di riferimento dell'elemento; le funzioni successive agiscono su quel sistema già modificato.
(Nota per chi conosce l'algebra lineare: matematicamente le matrici si moltiplicano da destra a sinistra. La lettura da sinistra a destra produce lo stesso risultato finale — è solo una prospettiva più intuitiva per i principianti.)
Confronto:
/* A: modifica il sistema di riferimento ruotandolo, poi trasla lungo il suo asse X ruotato */
.box-a { transform: rotate(45deg) translateX(100px); }
/* B: trasla nel sistema originale, poi ruota attorno all'origine dell'elemento */
.box-b { transform: translateX(100px) rotate(45deg); }
<div class="box">box</div>
.box {
transform: rotate(45deg) translateX(80px);
}
Risultati diversi!
rotate(45deg) translateX(100px): il sistema di riferimento ruota di 45°; la translateX sposta di 100px lungo l'asse X già ruotato → l'elemento finisce in diagonale a 45°.translateX(100px) rotate(45deg): prima trasla 100px a destra nel sistema originale, poi ruota di 45° attorno all'origine dell'elemento → l'elemento è 100px a destra e ruotato in place.Uso pratico — l'orbita:
@keyframes orbit {
from {
transform: rotate(0deg) translateX(80px);
}
to {
transform: rotate(360deg) translateX(80px);
}
}
rotate prima → il sistema di riferimento ruota. translateX(80px) si sposta sempre verso "destra" rispetto al sistema ruotato. Risultato: la luna orbita a 80px di distanza dal pianeta.
Se invertiamo l'ordine (translateX(80px) rotate(...)) la luna semplicemente ruota in place 80px a destra. Niente orbita.
Regola mentale: leggete le transform da sinistra a destra come istruzioni su un sistema di riferimento mobile — ogni funzione muove il "pavimento" su cui la successiva agisce.
transform Non Funziona su Elementi inlineUn'ultima trappola. Le transform non hanno effetto sugli elementi inline in Flow layout.
.inline-fella {
transform: rotate(-10deg); /* non fa nulla! */
}
<p>
Why <span class="inline-fella">Hello</span> there!
</p>
Perché? Gli inline element sono progettati per "andare con il flusso" del testo, con il minimo disturbo possibile. Distorcerli romperebbe il line-wrapping. Il browser semplicemente ignora la transform.
Le soluzioni (in ordine di preferenza):
Cambiare display: display: inline-block — l'elemento resta inline (sta sulla stessa riga del testo) ma accetta transform, width, height, padding-top/bottom.
.inline-fella {
display: inline-block;
transform: rotate(-10deg);
}
flex o grid risolve tutto automaticamente — i figli di flex/grid non sono mai inline.display: block: se non vi serve che l'elemento stia in-line con il testo.Why Hello there!
La transform viene ignorata: il testo resta identico.
inline-blockWhy Hello there!
Appena cambia display, la rotazione prende effetto.
inline-block funziona per stay-on-text-line, ma lo spazio verticale sopra/sotto diventa diverso da quello che avreste con inline puro. Testate sempre dopo il cambio.
Se siete a corto di tempo, saltatela e tornate a leggerla a casa. Il concetto chiave è: le proprietà translate, rotate, scale come proprietà autonome rendono le animazioni hover molto più semplici da scrivere.
Dal 2022 (Chrome 104, già prima su Firefox/Safari) CSS supporta translate, rotate e scale come proprietà autonome:
Caso d'uso concreto — card con hover:
Immaginate una card che ha sia una traslazione verticale sia un effetto di zoom al passaggio del mouse. Con transform shorthand dovete ripetere tutti i valori:
.card {
transform: translateY(-8px) scale(1);
}
.card:hover {
/* Dobbiamo ripetere translateY anche se non cambia! */
transform: translateY(-8px) scale(1.05);
}
Con le proprietà individuali, ogni proprietà è indipendente:
.card {
translate: 0 -8px;
scale: 1;
}
.card:hover {
scale: 1.05; /* solo questo cambia */
}
/* Vecchio modo */
.target {
transform: translateX(50%) rotate(30deg) scale(1.2);
}
/* Nuovo modo */
.target {
translate: 50% 0;
rotate: 30deg;
scale: 1.2;
}
<article class="card">card</article>
.card {
translate: 0 0px;
rotate: 0deg;
scale: 1;
}
Vantaggi:
Animazioni modulari. Con transform, per cambiare solo scale al hover dovete ripetere tutto:
.target { transform: translateX(50%) rotate(30deg) scale(1.2); }
.target:hover { transform: translateX(50%) rotate(30deg) scale(2); }
Con le proprietà individuali:
.target { translate: 50% 0; rotate: 30deg; scale: 1.2; }
.target:hover { scale: 2; }
Differenza importante: l'ordine di applicazione delle proprietà individuali è fisso: prima translate, poi rotate, poi scale (dall'esterno verso l'interno), a prescindere dall'ordine in cui le scrivete.
Con transform invece l'ordine dipende da come le scrivete (ogni funzione agisce sul sistema di riferimento già modificato dalla precedente, come abbiamo visto nella slide sulla composizione).
Se combinate entrambe:
.target {
translate: 50% 0; /* applicato per primo */
rotate: 30deg; /* applicato dopo */
scale: 1.2; /* applicato dopo */
transform: skew(5deg); /* applicato per ultimo (più interno) */
}
Regola pratica: usate le proprietà individuali per nuovo codice, soprattutto se userete animazioni. Usate transform classico quando vi serve un ordine di composizione non-standard (es. orbite).
Aprite CodePen e incollate il codice. Tre box, tre missioni. Usate DevTools per modificare le proprietà transform in tempo reale e osservate cosa succede.
<div class="centered-wrapper">
<div class="centered-card">Missione 1: centrami!</div>
</div>
<div class="card one">Box 1</div>
<div class="card two">Box 2 — al hover, ruotami di 15°</div>
<div class="card three">Box 3 — al hover, ruotami + scalami + spostami</div>
body {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 40px;
padding: 60px;
font-family: sans-serif;
}
.card {
background: deeppink;
color: white;
padding: 40px 16px;
text-align: center;
border-radius: 8px;
font-weight: bold;
transition: transform 300ms ease-out;
}
/* Missione 1: centraggio assoluto */
.centered-wrapper {
position: relative;
height: 200px;
grid-column: 1 / -1;
background: #f0f0f0;
}
.centered-card {
position: absolute;
top: 50%;
left: 50%;
/* ⬇️ aggiungete voi */
background: #8338ec;
padding: 20px 40px;
color: white;
border-radius: 8px;
}
/* Missione 2 e 3: qui dentro */
.card.two:hover {
/* ⬇️ aggiungete voi una transform */
}
.card.three:hover {
/* ⬇️ aggiungete voi una composizione di transform */
}
transform: translate(-50%, -50%) alla .centered-card. Il centro dell'elemento ora coincide con il centro del wrapper, indipendentemente dalle dimensioni del testo.transform: rotate(15deg) al :hover. Passate il mouse: la transizione è gratis grazie al transition sulla .card.transform: scale(1.1) rotate(5deg) translateY(-10px);transform: translateY(-10px) rotate(5deg) scale(1.1);transform-origin sulla Box 3 a top center e rifate il rotate. Come cambia?translate, rotate, scale autonome). L'effetto al hover funziona ancora? (Sì, e il CSS è più modulare.)transform| Concetto | Spiegazione |
|---|---|
| Mental model | L'elemento viene appiattito in una texture. Le transform deformano la texture, non ricalcolano il layout |
translate(x, y) |
Sposta. Percentuali relative all'elemento stesso (unico caso in CSS) |
scale(n) / scale(x, y) |
Moltiplica le dimensioni. Scala anche il testo dentro |
rotate(Ndeg | Nturn) |
Ruota attorno al transform-origin |
skew(x, y) |
Inclina. Usato per decorazioni diagonali |
transform-origin |
Il pivot. Default center. Cambia il comportamento di rotate e scale |
| Composizione | Ogni funzione agisce sul sistema di riferimento già modificato dalla precedente (leggete da sinistra a destra). L'ordine cambia il risultato |
| Inline gotcha | transform non funziona su elementi inline. Soluzione: display: inline-block o Flex/Grid |
| Proprietà individuali | translate, rotate, scale come proprietà autonome (2022+). Animazioni più modulari, ordine fisso |
vs top/left |
translate per centraggio e animazioni; top/left per nudge tipografici e stacking context |
| Effetto sul containing block | transform su un antenato rompe position: fixed (e scambia il containing block per absolute) sui figli (visto a Slide 26) |
Cerniera di collegamento: le transform sono il completamento del toolkit di positioning. Tutto quello che abbiamo imparato — uscire dal flusso, centrare, sovrapporre, nascondere — trova il suo ultimo tassello in queste funzioni. Nell'ultimo esercizio le mettiamo tutte insieme.
Prima dell'esercizio finale, un concetto che spiega perché le modali "fatte a mano" con solo div + z-index hanno sempre qualche bug in agguato.
Il browser espone un livello speciale, chiamato top layer ("livello superiore"), che si trova sopra qualsiasi stacking context CSS. Nessun z-index, per quanto alto, può competere con il top layer.
Chi può accedere al top layer?
Solo alcune API native del browser:
<dialog> aperto con dialog.showModal()<div popover> + popovertarget)Perché questa differenza è importante?
Una modale costruita con div + position: fixed + z-index: 9999 può essere ancora "battuta" da:
<select> nativo (che usa il top layer del sistema operativo)z-index più altoIl pattern moderno e accessibile:
const dialog = document.querySelector('dialog');
dialog.showModal();
dialog::backdrop {
background: rgba(0, 0, 0, 0.6);
}
<main class="page">...contenuto della pagina...</main>
<dialog open>
<h3>Conferma</h3>
<p>Testo della modale</p>
</dialog>
div + z-indexFunziona, ma resta dentro le normali regole di layering CSS.
<dialog> nel top layerLa modale nativa sale sopra tutto il resto senza inseguire i numeri.
<dialog> + showModal() gestisce automaticamente:
inert sul resto del documento::backdrop come overlay nativoL'esercizio che segue costruisce la modale "a mano" in CSS puro. Serve a capire cosa fa il browser dietro le quinte quando chiamate showModal(). Poi, in produzione, usate <dialog>.
Siete pronti per il gran finale. Costruirete una modale di conferma che combina ogni tecnica vista in questa lezione: position: fixed per l'overlay, z-index + stacking context per il layering, overflow per il body scrollabile, visually-hidden per l'accessibilità, e transform per il centraggio. Usate CodePen.
L'esercizio è organizzato in quattro livelli. Chi arriva al L1 ha già consegnato un esercizio valido; chi arriva al L4 ha in mano un pattern pronto per la produzione.
rgba(0,0,0,0.6)) copre tutta la pagina, anche durante lo scroll..modal con display: grid; grid-template-rows: auto 1fr auto e altezza massima 80vh. Solo .modal-body ha overflow-y: auto; header e footer restano fuori dallo scroll e restano naturalmente fermi — sticky non serve in questa architettura.isolation: isolate all'overlay. Controllate che nessun antenato dell'overlay abbia una transform (romperebbe position: fixed della modale)..visually-hidden per gli screen reader ("Chiudi finestra di dialogo").aria-modal="true" alla modale. Aggiungete inert al <main class="page"> dietro la modale.I seguenti quattro requisiti non si implementano in CSS; vanno oltre lo scope di questa lezione. Per chi vuole esplorarli a casa:
Per produzione, il modo corretto è usare l'elemento nativo <dialog> + dialog.showModal(), che gestisce tutti e quattro automaticamente insieme al top layer. Questo esercizio CSS "a mano" serve a capire cosa fa il browser quando chiamate showModal().
<!-- Il contenuto di "pagina" dietro la modale -->
<main class="page">
<h1>La mia pagina</h1>
<p>Lorem ipsum... (tanto contenuto per testare lo scroll)</p>
</main>
<!-- La modale -->
<div class="overlay">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<header class="modal-header">
<h2 id="modal-title">Conferma azione</h2>
<button class="close-btn">
<span aria-hidden="true">×</span>
<span class="visually-hidden">Chiudi finestra di dialogo</span>
</button>
</header>
<div class="modal-body">
<p>Contenuto molto lungo... (ripetete per testare lo scroll interno)</p>
</div>
<footer class="modal-footer">
<button>Annulla</button>
<button class="primary">Conferma</button>
</footer>
</div>
</div>
position: fixed + quattro offset a 0 + background: rgba(...). Per la modale: position: fixed + top: 50% + left: 50% + transform: translate(-50%, -50%). Struttura interna: .modal { display: grid; grid-template-rows: auto 1fr auto; max-height: 80vh; } e overflow-y: auto solo su .modal-body.isolation: isolate all'overlay. Se la modale non si centra, controllate che .overlay non abbia una transform.inert al <main class="page"> (non serve aria-hidden insieme a inert).position: absolute, a chi si aggancia?inert: no.)Le tre idee da portare a casa:
position significa cambiare quale algoritmo governa un elemento.z-index dialoga con position. overflow crea scroll container che governano sticky. transform crea stacking context e può rompere fixed. La causa dei bug è quasi sempre in un'interazione che non avevate mappato.Mappa diagnostica — Cosa guardare prima:
| Sintomo osservato | Prima cosa da controllare | Poi |
|---|---|---|
position: fixed scrolla |
transform / filter / perspective su antenati |
Usare findCulprits() |
z-index alto ignorato |
Stacking context di un antenato | Salire nel DOM con DevTools |
sticky non si attacca |
overflow: hidden/auto/scroll su un antenato |
Altezza del container + align-self in Grid/Flex |
| Modale non centrata | transform sull'overlay |
isolation: isolate sull'overlay senza transform |
| Tooltip tagliato | overflow: hidden su una card antenata |
Sollevare il tooltip fuori dal container |
| Tab passa su elementi nascosti | opacity: 0 o visibility: hidden sbagliati |
inert per disattivare tutto il sottoalbero |
Grazie per l'attenzione. Ora quando vedrete una modale centrata, un header sticky, un overlay, o un tooltip che sborda da uno scroll container… saprete ripararlo quando si rompe.