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: Tornerete a costruire un overlay di questo tipo nella lezione dedicata a <dialog> e all'API popover, dove il browser farà gran parte del lavoro al posto vostro. L'esercizio finale di questa lezione, invece, è un recap che mette insieme position, overflow, hiding e transform su una pagina catalogo.
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.
| 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.) C'è anche un ponte con la lezione di Grid: lì 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.
Cosa vedremo:
position: sticky: il valore che scorre con la pagina e poi si blocca — come un ascensore che si ferma a un pianoposition: sticky — Metà relative, metà fixedposition: sticky è il valore più recente di position. L'idea è semplice da enunciare, subdola da capire:
Pensatelo come un magnete montato sull'elemento e attratto dal bordo del viewport. Finché il bordo è lontano, l'elemento scorre normalmente insieme al contenuto, come relative. Quando arriva alla distanza indicata da top, il magnete si aggancia e l'elemento resta lì, quasi come fosse fixed. Ma l'aggancio ha un limite: lo sticky non può uscire dal suo container, quindi quando il genitore finisce, anche lui viene trascinato via.
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.
Quando sticky non funziona, quasi mai il problema è position: sticky in sé. Di solito state sbagliando il contesto intorno all'elemento.
Bug 1: un antenato con overflow. Sticky si attacca al primo scroll container che incontra risalendo il DOM. Se un genitore, nonno o bisnonno ha overflow: auto, scroll o hidden, il riferimento non è più il viewport ma quel container. Risultato: lo sticky si incolla dentro quel box, oppure sembra non fare nulla se quel box non scrolla davvero.
Bug 2: non c'è abbastanza spazio per muoversi. Sticky ha bisogno di un tratto di strada. Se il container è alto quanto lo sticky, o quasi, non esiste una fase visibile in cui l'elemento scorre e poi si attacca. In pratica sembra rotto, ma in realtà non ha spazio di manovra.
Bug 3: Grid o Flex lo stirano. In Grid e Flex gli item vengono spesso stretchati sull'asse trasversale. Se la sidebar sticky occupa già tutta l'altezza della sua cella, non può "viaggiare". Il fix classico è aggiungere un wrapper interno e usare align-self: start sul wrapper, così la cella smette di stirarsi e lo sticky torna libero di muoversi.
Checklist mentale: se sticky non si attacca, controllate in quest'ordine gli overflow degli antenati, lo spazio disponibile nel container, e lo stretch implicito di Grid/Flex. Nove volte su dieci il problema è uno di questi tre.
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 (una classe con un ruleset specifico) |
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;
}
<a href="/blog/accessibilita">
Leggi di più
<span class="visually-hidden">
sull'articolo Accessibilità web
</span>
</a>
Qui .visually-hidden è spesso preferibile a aria-label. Il nome accessibile del link contiene già il testo visibile — "Leggi di più" — e lo estende con il contesto, invece di sostituirlo con una frase diversa. Questo aiuta anche chi usa software di riconoscimento vocale: può dire "Leggi di più", perché quelle parole fanno davvero parte del nome accessibile. aria-label resta utilissimo soprattutto per controlli senza testo visibile, come un bottone con sola icona.
aria-hidden="true" — il contrario di visually-hidden: visibile agli occhi, invisibile agli screen reader:
<button>
<span aria-hidden="true">🔍</span>
Cerca
</button>
Qui la lente è solo decorativa: il significato vero è già nel testo "Cerca". Con aria-hidden="true" dite allo screen reader di ignorare quel pezzetto visibile, così il controllo viene annunciato semplicemente come "Cerca". Questo è il caso giusto per aria-hidden: un frammento visibile, non focusable, che non aggiunge informazione utile.
aria-hidden non rimuove gli elementi dall'ordine di Tab. Quindi usatelo solo su pezzi decorativi non interattivi. Se lo mettete su un link, un bottone, o un contenitore con elementi focusable dentro, create un oggetto raggiungibile da tastiera ma invisibile ai lettori di schermo.
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).
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 (es. aggiungere contesto a “Leggi di più”) |
aria-hidden="true" |
Volete che gli screen reader ignorino un elemento visibile ma decorativo (icone, separatori, duplicati) |
inert |
Volete disattivare tutto di un sottoalbero (background di una modale aperta) |
Regola d'oro per l'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 |
| Reflow 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ì, attenzione |
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. 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:
Pensate a un'animazione di tipo "pop" (scale 1 → 1.1 → 1).
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.
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-word {
transform: rotate(-10deg); /* non fa nulla! */
}
<p>
Tu mi fai <span class="inline-word">girar</span> come fossi una bambola
</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-word {
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.Tu mi fai girar come fossi una bambola
La transform viene ignorata: il testo resta identico.
inline-blockTu mi fai girar come fossi una bambola
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.
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.
Costruirete una mini-pagina catalogo in cui ogni pezzo motiva un concetto visto a lezione. Solo HTML + CSS, niente JavaScript. Usate CodePen.
| Famiglia | Concetti messi in pratica |
|---|---|
| Position | i 4 valori "attivi": relative, absolute, fixed, sticky (più static come default su <main>, per contrasto) |
| Overflow | overflow-x: auto sul carosello + overflow: clip per contenere lo zoom hover |
| Hiding | .visually-hidden (visibile solo agli screen reader) + aria-hidden="true" (visibile solo agli occhi) |
| Transform | rotate sul badge con transform-origin all'angolo + chain rotate + scale sull'hover del badge (l'ordine conta) |
┌─────────────────────────────────┐
│ [STICKY HEADER · logo · menu] │ ← .page-header → position: sticky
├──────────────────────────────────┤
│ <main class="page"> │ <main> resta static (default)
│ ┌───────┐ ┌───────┐ ┌───────┐ →│ ← .gallery → overflow-x: auto
│ │ IMG │ │ IMG │ │ IMG │ │ .card-image → overflow: clip
│ │[-30%]◄┼─┤badge │ │ │ │ .product-card → position: relative
│ └───────┘ └───────┘ └───────┘ │ .card-badge → position: absolute
│ hover badge: rotate+scale │ + rotate(-8deg)
├──────────────────────────────────┤
│ Footer │
└──────────────────────────────[↑]─┘ ← .fab → position: fixed
button × · ♥ con .visually-hidden
★ decorativo con aria-hidden="true"
Ogni livello pratica una famiglia di concetti con 2-3 step concreti. Chi finisce L1 ha consegnato un esercizio valido; chi finisce L4 ha un recap completo.
.page-header → position: sticky; top: 0;.product-card → position: relative; (containing block per il badge).card-badge → position: absolute; top: 8px; right: 8px;.fab (bottone "↑ torna su" in basso a destra) → position: fixed; bottom: 24px; right: 24px;<main class="page"> → niente position. È static, il default. Lo lasciate così per contrasto.auto e clip non sono sinonimi.gallery (la <ul> con le card) → display: flex; overflow-x: auto;. Le card non vanno a capo: scrollano in orizzontale..card-image → overflow: clip;. Sull'<img>:hover mettete transform: scale(1.06) con una transition. Il bordo arrotondato ritaglia lo zoom.clip è preferibile a hidden? Cosa significa per il sistema di sticky/transform?.visually-hidden sui bottoni icon-only "×" (chiudi banner) e "♥" (wishlist): aggiungete <span class="visually-hidden">Aggiungi ai preferiti</span> accanto all'icona. Definite voi la classe con il pattern visto a lezione (clip-based).aria-hidden="true" sulla stella decorativa ★ accanto al titolo "Bestseller": gli occhi la vedono, gli screen reader la ignorano..visually-hidden nasconde agli occhi, aria-hidden nasconde agli screen reader.transform-origin)Tutto il transform vive sul badge. Stato base e stato hover della card — ma il transform che cambia è sempre quello del badge.
.card-badge (stato base) → transform: rotate(-8deg); con transform-origin: top right; e transition: transform .2s. L'origin "cuce" il badge all'angolo della card invece di farlo ruotare attorno al proprio centro: provate a togliere la riga transform-origin e vedete come il badge "scivola" dentro la card..product-card:hover .card-badge → transform: rotate(-12deg) scale(1.1);. Due funzioni in chain — l'ordine conta (recap Slide 54): scale dopo rotate ingrandisce nella direzione già ruotata.transform è sul badge, che non ha figli con position: fixed — quindi non rompe niente. Ma se invece mettessi transform sulla .product-card e ci avessi dentro un elemento con position: fixed, cosa succederebbe? (Riferimento Slide 26.)<header class="page-header">
<strong>Bacheca</strong>
<nav>Home · Catalogo · Account</nav>
<button class="close-banner">
<span aria-hidden="true">×</span>
<span class="visually-hidden">Chiudi banner promozionale</span>
</button>
</header>
<main class="page">
<h1>I bestseller della settimana <span aria-hidden="true">★</span></h1>
<ul class="gallery">
<li class="product-card">
<div class="card-image">
<img src="https://picsum.photos/seed/a/400/300" alt="Sedia in legno chiaro">
</div>
<span class="card-badge">-30%</span>
<h3>Sedia Linnmon</h3>
<p>€ 89</p>
<button class="wishlist">
<span aria-hidden="true">♥</span>
<span class="visually-hidden">Aggiungi ai preferiti</span>
</button>
</li>
<!-- Duplicate la card 4-5 volte per testare il carosello -->
</ul>
<p>Aggiungete tanto contenuto qui sotto per testare lo scroll verticale e il comportamento sticky dell'header.</p>
</main>
<button class="fab">
<span aria-hidden="true">↑</span>
<span class="visually-hidden">Torna su</span>
</button>
transform a un antenato del FAB, cosa succede al suo position: fixed?overflow: clip su .card-image invece di overflow: hidden? Cosa cambia per il sistema di scroll/sticky?position: relative da .product-card, dove finisce il badge?overflow: clip sulla .product-card invece che sulla .card-image?Una soluzione possibile. Confrontate con la vostra: se avete fatto scelte diverse ma che funzionano, ottimo — qui interessa il perché, non l'identità byte-per-byte.
/* Reset minimo */
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; }
/* L1 — Positioning */
.page-header {
position: sticky; /* scorre con la pagina, poi si blocca */
top: 0;
background: #fff;
border-bottom: 1px solid #ddd;
padding: 12px 24px;
display: flex;
gap: 16px;
align-items: center;
z-index: 10;
}
.page {
/* niente position: è static, il default. Citato per contrasto. */
padding: 24px;
}
.product-card {
position: relative; /* containing block per il badge */
list-style: none;
width: 240px;
flex-shrink: 0; /* non si rimpicciolisce nel flex carosello */
border: 1px solid #ddd;
border-radius: 12px;
padding: 12px;
background: #fff;
}
.card-badge {
position: absolute; /* esce dal flusso, ancorato alla card */
top: 8px;
right: 8px;
background: #e11d48;
color: #fff;
padding: 4px 10px;
border-radius: 4px;
font-weight: 700;
transform: rotate(-8deg); /* L4 — stato base */
transform-origin: top right; /* L4 — "cuce" il badge all'angolo;
senza, il rotate ruoterebbe attorno al centro
e il badge "scivolerebbe" dentro la card */
transition: transform .2s;
}
.fab {
position: fixed; /* ancorato al viewport */
bottom: 24px;
right: 24px;
width: 56px;
height: 56px;
border-radius: 50%;
border: none;
background: #1e40af;
color: #fff;
font-size: 24px;
cursor: pointer;
z-index: 20;
}
/* L2 — Overflow */
.gallery {
display: flex;
gap: 16px;
overflow-x: auto; /* scrollbar orizzontale solo se serve */
padding: 8px 0;
list-style: none;
margin: 0;
}
.card-image {
overflow: clip; /* taglia lo zoom SENZA creare scroll container.
Con `hidden` creeremmo uno scroll container che
interferirebbe con sticky/transform discendenti.
`clip` taglia visivamente e basta. */
border-radius: 8px;
}
.card-image img {
width: 100%;
height: auto;
display: block;
transition: transform .3s;
}
.product-card:hover .card-image img {
transform: scale(1.06); /* lo zoom esce dai bordi → clippato da overflow: clip */
}
/* L3 — Hiding accessibile */
.visually-hidden {
/* Visibile agli SCREEN READER, invisibile agli OCCHI.
Pattern clip-based standard (più robusto di display: none,
che invece nasconderebbe a tutti). */
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* La ★ accanto al titolo usa solo aria-hidden="true" nell'HTML:
è il CONTRARIO COMPLEMENTARE di .visually-hidden — visibile agli occhi,
muta agli screen reader. Niente CSS necessario. */
.wishlist,
.close-banner {
background: transparent;
border: none;
font-size: 20px;
cursor: pointer;
}
/* L4 — Transform: chain di due funzioni sull'hover del badge */
.product-card:hover .card-badge {
transform: rotate(-12deg) scale(1.1);
/* Chain di due funzioni — l'ordine conta (recap Slide 54).
scale DOPO rotate: il badge prima ruota a -12deg, poi viene ingrandito
nella direzione già ruotata. Provate a invertire — l'effetto cambia.
Nota Slide 26: qui il transform è sul badge, che non ha figli con
position: fixed — quindi non rompe niente. Se invece mettessi
transform sulla .product-card e ci avessi dentro un fixed, quel
fixed sceglierebbe la card come containing block invece del viewport. */
}
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.Grazie per l'attenzione..