ITS Web Design e Strategie Digitali
ITS Academy I-CREA @ CFP Bauer

Guida interattiva a

Positioned Layout e Transforms

Positioning, layering, overflow, sticky e hidden content

Adattato da Josh W. Comeau - CSS for JS Developers + CSS Transforms

Rompere le Regole del Flusso

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.

Cosa imparerete

  • Le quattro varianti di position: relative, absolute, fixed, sticky
  • Come gestire le sovrapposizioni con z-index e gli stacking context
  • Come funzionano davvero overflow e gli scroll container
  • Come nascondere contenuto nel modo giusto (anche per l'accessibilità)
  • Come spostare, scalare e ruotare gli elementi con le transform

Ricordate la lezione di Grid?

Nella 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.

I Sistemi di Layout CSS

Riprendiamo la panoramica che abbiamo già visto, e spostiamo i riflettori.

Flow Layout

display: block / display: inline

Default del browser. Gli elementi si impilano, nessuna sovrapposizione.

Flexbox Layout

display: flex

Layout mono-dimensionale, elementi che si adattano al contenuto.

Grid Layout

display: grid

Griglia bidimensionale con righe e colonne definite.

Positioned Layout

position: relative | absolute | fixed | sticky

Elementi che possono sovrapporsi, uscire dal flusso, rimanere ancorati durante lo scroll. Oggi tocca a lui!

Un mini-algoritmo dentro l'altro

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.

Attivare Positioned Layout

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 superpoteri
  • absolute — l'elemento esce dal flusso e si piazza dove vogliamo noi
  • fixed — l'elemento resta ancorato al viewport (la finestra del browser), anche durante lo scroll
  • sticky — 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.

E il valore di default?

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; */
}

HTML di partenza

<div class="row">
  <div class="box">1</div>
  <div class="box highlight">2</div>
  <div class="box">3</div>
</div>

position: static

1
2
3

Default del browser: l'elemento resta nel flusso normale.

position: relative

1
2
3

Entrate in Positioned Layout: ora potete usare offset e layering.

Un nome fuorviante

"Statically-positioned" è un modo un po' fuorviante di dire "non-posizionato". Non è un sistema di posizionamento, è l'assenza di position.

position: relative - Sbloccare gli Offset

relative è 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:

1. Sblocca nuove proprietà CSS

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.

2. Rende quegli offset relativi alla posizione naturale

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.

HTML di partenza

<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>
top: 0px
left: 0px
1
2
3
4
5
.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.

Valori negativi

I valori negativi funzionano benissimo: left: -10px produce lo stesso effetto visivo di right: 10px. Scegliete quello che si legge meglio nel vostro caso.

Movimento Visivo vs Movimento di Layout

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.

HTML di partenza

<div class="stack">
  <div class="box pink">A</div>
  <div class="box dark">B</div>
  <div class="box dark">C</div>
</div>

Naturale

A
B
C
/* niente */

Con margin-top

A
B
C
.pink {
  margin-top: 20px;
}

B e C scendono con A.

Con position: relative

A
B
C
.pink {
  position: relative;
  top: 20px;
}

B e C non si muovono.

Qualche analogia

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.

Una conseguenza meno ovvia: la larghezza

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.

Una terza opzione in arrivo

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 Flusso

Finora 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;
}

Due cose cambiano rispetto a relative

  1. Gli offset top/left/right/bottom non sono più relativi alla posizione naturale. Sono distanze misurate dai bordi del contenitore (vedremo fra poco quale contenitore, esattamente).
  2. L'elemento esce completamente dal flusso. Per il layout, è come se non esistesse più.

HTML di partenza:

<div class="frame">
  <p>Paragrafo uno.</p>
  <p>Paragrafo due.</p>
  <p>Paragrafo tre.</p>
  <div class="pink-box"></div>
</div>
top: 20px
offset orizzontale: 25%
anchor:

Paragrafo uno.

Paragrafo due.

Paragrafo tre.

.pink-box {
  position: absolute;
  top: 20px;
  left: 25%;
}

A cosa serve absolute?

È lo strumento giusto per elementi che devono galleggiare sopra il contenuto:

  • Badge e pulsanti di chiusura di una card
  • Tooltip, dropdown, popover
  • Forme decorative dietro un componente
  • Etichette ancorate a un'immagine

In tutti questi casi, vogliamo che l'elemento non influenzi il layout intorno.

Elementi "Fantasma"

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.

Conseguenza 1: l'ordine nel DOM non conta (quasi mai)

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.

Conseguenza 2: se non gli do un'ancora, eredita la posizione di flusso

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.

Normale

Paragrafo uno.
Paragrafo due.
Paragrafo tre.

Tutti gli elementi restano nel flusso, uno sotto l'altro.

absolute senza anchor

Paragrafo uno.
Paragrafo due.
Paragrafo tre.

La box rosa resta nel suo punto naturale, ma il terzo elemento le passa sotto.

Conseguenza 3: i genitori possono "collassare"

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.

.child:
.parent
.child (200px)

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.

Se dovete "riparare" un genitore collassato...

...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.

Sovrapporre con 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?

HTML di partenza

<section class="hero">
  <div class="hero-image">Immagine</div>
  <h3 class="hero-title">Titolo sopra l'immagine</h3>
</section>

Grid overlap

Immagine
Titolo sopra l'immagine
.hero {
  display: grid;
}
.hero > * {
  grid-area: 1 / 1;
}
  • Entrambi gli elementi restano nel layout
  • Il contenitore si dimensiona naturalmente
  • Se il titolo cresce, il contenitore cresce con lui

position: absolute

Immagine
Titolo sopra l'immagine
.hero {
  position: relative;
}
.hero-title {
  position: absolute;
  bottom: 16px;
  left: 16px;
}
  • Il titolo esce dal flusso
  • Il contenitore non sa che il titolo c'è
  • Servono offset numerici espliciti

Regola pratica

  • Preferite Grid overlap quando gli elementi devono restare in layout, il contenitore deve adattarsi al contenuto, o volete evitare calcoli numerici fragili.
  • Usate position: absolute quando l'elemento è anchored UI (badge, close button, tooltip, dropdown, popover, decorazioni) e non deve riservare spazio nel layout.

Non è una scelta unica per tutta la pagina

Nelle UI reali li combinate: Grid per il layout principale, absolute per gli accessori ancorati ai componenti.

Il Containing Block

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.

L'algoritmo del browser per trovare il containing block di un absolute

  1. Parti dall'elemento
  2. Sali su un antenato alla volta
  3. Fermati al primo antenato con position diversa da static (relative, absolute, fixed, sticky)
  4. Quel primo antenato posizionato è il containing block
  5. Se nessun antenato è posizionato, usa il viewport (la finestra del browser) come containing block

HTML di partenza

<div class="level-1">
  <div class="level-2">
    <div class="level-3">
      <div class="pink"></div>
    </div>
  </div>
</div>
antenato relative:
livello 1
livello 2
livello 3
.level-1 { position: relative; }
.pink {
  position: absolute;
  top: 0;
  right: 0;
}

Il meccanismo standard di "contenimento"

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.

Nota sul padding

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.

Il Trucco per Centrare (e una Scorciatoia)

Gli elementi absolute hanno un altro trucco nella manica: possiamo centrarli perfettamente dentro il loro containing block.

La ricetta a 4 ingredienti

.box {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100px;
  height: 100px;
  margin: auto;
}

Serve tutto e quattro:

  1. position: absolute
  2. Tutte e quattro le proprietà di offset a 0
  3. Una width e una height esplicite
  4. margin: auto

Il browser vede i quattro 0 e capisce: "vuole stare centrato in entrambe le dimensioni". margin: auto distribuisce lo spazio residuo equamente.

HTML di partenza

<div class="frame">
  <div class="box"></div>
</div>
ingredienti:

Ancora rilevante oggi?

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).

La scorciatoia: inset

Scrivere 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 */
}

Supporto browser

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.

Provate voi!

Puzzle: Dove Va la Box Rosa?

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).

HTML di partenza

<!-- 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>

CSS di partenza

.frame {
  padding: 16px;
  border: 2px solid silver;
  margin-bottom: 12px;
}

.pink-box {
  position: absolute;
  top: 0;
  right: 0;
  width: 40px;
  height: 40px;
  background: deeppink;
}

Cosa esplorare

  • Scenario 1: aggiungete position: relative solo al frame esterno di turno. Dove finisce la box?
  • Scenario 2: aggiungete position: relative solo al frame interno. La box si sposta?
  • Provate a dare position: relative a entrambi i frame. Chi vince? (ricordate: il primo antenato posizionato che incontra salendo)
  • Rimuovete ogni position dai frame. Dove finisce ora la box? (suggerimento: pensate al viewport)
  • Sfida bonus: aggiungete padding: 16px a un frame con position: relative. Il padding viene rispettato dalla box rosa?

Chi Sta Sopra a Chi?

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.

Un ponte con la lezione di Grid

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.

Cosa copriamo

  • Come il browser decide l'ordine di disegno di default
  • La proprietà z-index e le sue regole
  • I stacking context: che cosa sono, come nascono, perché creano problemi
  • La proprietà isolation: l'antidoto pulito contro le "z-index wars"

Come Dipinge il Browser

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.

Il processo in due fasi

  1. Il browser dipinge tutti gli elementi non-posizionati (Flow, Flexbox, Grid), seguendo l'ordine del DOM.
  2. Poi dipinge sopra tutti gli elementi posizionati, sempre seguendo l'ordine del DOM.

Demo: due box che si sovrappongono

<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>

Stato 1 - nessuno posizionato

A
B

La box B (rosa) è sopra, perché viene dopo nel DOM.

Stato 2 - solo A posizionata

A
B

La box A (argento) sale sopra, perché ora è posizionata mentre B non lo è.

Stato 3 - entrambe relative

A
B

Tornano nell'ordine del DOM; B di nuovo sopra.

Un dettaglio in più di Flow layout

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 Z

Cosa 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.

Regole di z-index

  1. Funziona solo con elementi posizionati (più un paio di eccezioni: i grid children e i flex children, dove z-index funziona lo stesso). Nel Flow layout puro non ha effetto.
  2. Accetta solo numeri interi: z-index: 1.5 non è valido.
  3. Il valore di default è 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.
  4. Sono ammessi valori negativi (z-index: -1), ma portano più grattacapi che benefici. In questa lezione non li usiamo.

HTML di partenza

<div class="box first">A</div>
<div class="box second">B</div>
z-index (box A): 0
A
B
.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.

Bridge con Grid

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.

Che Cos'è uno Stacking Context

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.

La regola che risolve il 90% dei bug

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.

Quando Nasce un Nuovo Stacking Context

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.

I Discendenti Sono "Schiacciati" Dentro il Context

Conseguenza importante

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>
Context A - z-index: 1
Child z-index: 999
Context B - z-index: 2

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.

Il figlio è prigioniero del genitore

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.

Perché 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.

Il caso tipico: tre card di pricing con un header sticky

Una pagina ha:

  • Un header in cima: position: fixed; z-index: 2
  • Tre card di prezzi: .card { position: relative; z-index: 1 }, e la card centrale con z-index: 2 per emergere sopra le altre

Tutto sembra funzionare. Poi arriva la segnalazione: scrollando, l'header passa "in mezzo" alle card. Sotto la card centrale, sopra le laterali. Un disastro.

Perché?

Perché header e card sono tutti nello stesso stacking context (quello della root della pagina). I loro z-index si confrontano tra loro:

  • Le card laterali (z-index: 1) → sotto l'header (z-index: 2). OK.
  • La card centrale (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 Collaterali

Abbiamo 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.

HTML di partenza

<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>
isolation su .pricing:
Header (z-index: 2)
Starter
Pro
Enterprise
.pricing {
  /* isolation: isolate; */
}

Scena che riproduce il bug: header sticky + tre card sovrapposte. Toggle isolation: isolate sul wrapper .pricing:

  • OFF: scrollando, l'header passa in mezzo alle card (bug)
  • ON: le tre card restano tutte sotto all'header, come un gruppo unico

Euristica utile

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.

A Volte Non Serve Affatto z-index

Prima 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.

Esempio: una card con blob decorativi dietro

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.

Con z-index

Hello World

Card sopra ai blob, grazie al layering esplicito.

Con ordine del DOM

Hello World

Stesso risultato visivo, ma senza introdurre numeri globali.

Attenzione: l'ordine del DOM è anche l'ordine di tabulazione!

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.

Provate voi!

Riparate il Bug dell'Header

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.

HTML di partenza

<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>

CSS di partenza

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; }

Cosa esplorare

  • Scrollate senza modifiche: vedete l'header che "passa in mezzo" alle card?
  • Aggiungete isolation: isolate a .pricing. Rifate scroll: il bug è sparito?
  • Provate invece a scrivere position: relative; z-index: 1 su .pricing. Funziona lo stesso?
  • Rimuovete entrambe le soluzioni e provate ad abbassare il z-index della card centrale a 1. Il bug è risolto, ma a che costo visivo? (suggerimento: l'enfasi sulla card centrale)
  • Sfida bonus: aprite i DevTools e usate "CSS Stacking Context Inspector" (estensione Chrome/Firefox) per vedere quanti stacking context ci sono prima e dopo la fix.

Riepilogo: Livelli, 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.

Sezione 3: fixed, overflow, Scroll Container

Con 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.
  • Scroll container: un meccanismo nascosto di CSS che overflow attiva senza dircelo.

Un ponte con la lezione di Grid

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 DOM

position: 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;
}

HTML di partenza

<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.

Senza anchor point

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.

Il Pattern Classico: Modale a Schermo Intero

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;
}

HTML di partenza

<div class="modal" role="dialog" aria-modal="true">
  <h3>Conferma</h3>
  <p>Testo della modale</p>
</div>

Cosa succede:

  1. I quattro offset a 0 dicono: "occupa tutto il viewport".
  2. width e height espliciti riducono l'elemento.
  3. 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.

Viewport (prima)

I quattro offset a 0 dilatano la box al viewport.

Con width/height

Le dimensioni esplicite riducono la scatola.

Con margin: auto

margin: auto distribuisce lo spazio residuo su tutti e quattro i lati.

Provate il contrasto

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.

Il Gotcha del Containing Block

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;
}

HTML di partenza

<div class="container">
  <div class="fixed">.fixed</div>
  <div class="content">...contenuto lungo...</div>
</div>
.fixed

.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.

Le proprietà colpevoli

  • transform (anche translate, rotate, scale, ecc. quando usati come proprietà singole)
  • filter
  • will-change: transform
  • perspective
  • backdrop-filter

Perché è un bug cattivissimo

In 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 Box

Passiamo 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.

Name: Pablo Diego Jose Francisco de Paula Juan Nepomuceno Maria de los Remedios Cipriano de la Santisima Trinidad Ruiz y Picasso
Born: 25 October 1881

Nella prossima slide vediamo come domare questo comportamento.

I Quattro Valori (più 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 */
}

HTML di partenza

<div class="wrapper">
  <p>Contenuto molto piu lungo dell'altezza disponibile...</p>
</div>

visible

Riga 1

Riga 2

Riga 3

Riga 4

Riga 5

Il contenuto esce dal box e può invadere quello sotto.

auto

Riga 1

Riga 2

Riga 3

Riga 4

Riga 5

Scorre solo quando serve.

hidden

Riga 1

Riga 2

Riga 3

Riga 4

Riga 5

Taglia ma crea comunque uno scroll container.

clip

Riga 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.

La Differenza Fondamentale: Tagliare o Scorrere?

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: hidden

Non vedete le barre, ma il contenitore può comunque scorrere via focus o script.

overflow: scroll

Stesso 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.

Scroll Container: il TARDIS di CSS

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%;
}

HTML di partenza

<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:
Card 1
Card 2
Card 3
Card 4
Card 5
Card 6

Le tre regole dello scroll container

  1. Si crea automaticamente quando impostate overflow a scroll, auto, hidden (o clip su un solo asse in alcuni casi).
  2. Gestisce entrambi gli assi insieme. Controllare un solo asse non è il default naturale; è possibile (es. 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.
  3. I figli sono prigionieri. Una volta entrati nel portale, non escono più dal bordo del container.

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 */

Il rovescio di clip

overflow: 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.

Provate voi!

overflow: hidden vs overflow: clip — Come si Comportano con la Tastiera

Mini-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.

HTML di partenza

<div class="container">
  <div class="inner">
    <button class="btn">Visibile</button>
    <button class="btn">Nascosto (ma raggiungibile?)</button>
  </div>
</div>

CSS di partenza

.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;
}

Cosa esplorare

  • Con 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.
  • Con 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.
  • Conclusione pratica: usate clip solo per contenuto decorativo. Se dentro ci sono elementi interattivi (bottoni, link, input), usate hidden oppure ripensate il layout.

Scroll Orizzontale: la Ricetta Pratica (Tab Bar Mobile)

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.

La ricetta

.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;
}

HTML di partenza

<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.

Perché sono nella stessa ricetta

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.

Provate voi!

Il Caso del fixed Sabotato

Aprite 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.

HTML di partenza

<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>

CSS di partenza

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;
}

Cosa esplorare

  • Scrollate la pagina: l'header segue lo scroll o resta fermo?
  • Aprite i DevTools, selezionate l'header, e guardate la scheda Computedposition. È davvero fixed?
  • Ora selezionate .page-wrapper e cercate proprietà sospette (transform, filter, will-change, perspective). Quale di queste, anche con valore "innocuo", rompe fixed?
  • Fix: rimuovete (o commentate) la proprietà colpevole. L'header torna magicamente a comportarsi da fixed.
  • Sfida bonus: rimettete 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).
  • Sfida bonus 2: aggiungete overflow: hidden a .page-wrapper senza filter. Il fixed si rompe? No. overflow non rompe fixed (ma rompe sticky — lo vediamo nella prossima sezione).

Riepilogo: Fuori dal Flusso, Dentro la Viewport

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.

Sezione 4: Il Magico sticky e l'Arte di Nascondere

In 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 piano
  • I quattro modi classici in cui sticky fallisce e come diagnosticarli
  • Hidden content: display: none, visibility: hidden, opacity: 0, visually-hidden, aria-hidden, inert
  • Perché ogni tecnica di hiding ha implicazioni per l'accessibilità e i lettori di schermo

Un ponte con la lezione di Grid

Nella 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à fixed

position: sticky è il valore più recente di position. L'idea è semplice da enunciare, subdola da capire:

Il mental model

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:

  1. Serve almeno un offset (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".
  2. Non esce mai dal suo genitore. Quando il container scorre via, lo sticky scorre con lui.

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>
sticky:

Sezione 1

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.

Sezione 2

Paragrafo quattro: il nuovo header spinge fuori il precedente.

Paragrafo cinque di filler per allungare la sezione.

Paragrafo sei, ancora filler.

Sezione 3

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.

Sticky Occupa Spazio Davvero

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;
}

HTML di partenza

<section class="container">
  <header class="main-box">Header</header>
  <p>Contenuto...</p>
</section>

Con sticky

Header sticky

L'header continua a occupare spazio nel layout.

Con fixed

Header fixed

Il contenuto risale: l'header non riserva piu spazio.

Cambiando da sticky a fixed:

  • Gli altri fratelli risalgono per riempire il buco lasciato dal fantasma
  • Il container si rimpicciolisce
  • Il contenuto sembra "schiacciarsi"

Tornando a sticky:

  • Gli altri fratelli restano fermi
  • Il container conserva l'altezza
  • L'elemento si muove rimanendo in-flow

Significato pratico

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.

Bug #1: Un Antenato con overflow Rompe lo Sticky

Ecco 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;
}

HTML di partenza

<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.

overflow antenato:
Header sticky

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.

Bug Sticky #2, #3, #4 e Come Riconoscerli

# 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

Il callback alla lezione di Grid

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;
}

HTML di partenza

<div class="layout">
  <div class="sidebar-wrapper">
    <aside class="sidebar">Indice</aside>
  </div>
  <main>Contenuto lungo...</main>
</div>
scenario:
Sticky (in cerca di spazio)

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.

Provate voi!

Sticky: Capire Perché Non si Attacca

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).

HTML di partenza

<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>

CSS di partenza

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;
}

Cosa esplorare

  • Scenario 1: lo sticky si attacca, ma dentro lo scenario, non sulla pagina. Perché? Rimuovete overflow: auto e max-height: torna a funzionare normalmente.
  • Scenario 2: aggiungete abbastanza contenuto (paragrafi ripetuti) al div per dargli altezza. Sticky si riattiva quando il container diventa scrollabile.
  • Scenario 3: il wrapper esiste già nello starter. Provate ad aggiungere e togliere 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.
  • Sfida bonus: provate a cambiare top: 0 in top: 20px. Lo sticky si ferma 20px sotto il bordo, non attaccato.
  • Sfida bonus 2: con DevTools, selezionate lo sticky dello Scenario 1 e guardate la scheda Layout: vedete il badge sticky? I browser moderni vi segnalano anche il "contenitore di attivazione" di sticky.

Come Nascondere un Elemento (ne Vale la Pena Sapere)

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 (lascia il buco) No No
opacity: 0 No Sì! ⚠️ Sì! ⚠️
.visually-hidden (classe) No No (quasi) Sì se elemento focusable (apposta)
aria-hidden="true" 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; }

HTML di partenza

<div class="row">
  <div class="item a">A</div>
  <div class="item b">B</div>
  <div class="item c">C</div>
</div>

display: none

A
C

Il layout si richiude: lo spazio di B sparisce.

visibility: hidden

A
C

B non si vede, ma il suo buco resta li.

opacity: 0

A
B
C

B 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, inert

opacity: 0 — l'elemento diventa trasparente, ma non è nascosto davvero:

.flourish { opacity: 0.5; }  /* semi-trasparente */
.flourish { opacity: 0; }    /* invisibile... ma ancora lì */

Problema

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.

Trappola

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: 0

Il secondo bottone non si vede, ma continua a esistere.

.visually-hidden

Voi vedete solo l'icona; il testo resta disponibile per gli screen reader.

inert

Contenuto dietro la modale

Link disattivato

Il blocco resta visibile, ma e trattato come non interattivo.

Attenzione alla ridondanza

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.

Riepilogo: Appiccicoso e Invisibile

Sticky

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

Hidden content

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.

Sezione 5: Le transform

Siamo 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:

  • Spostare un elemento (translate)
  • Ingrandire o rimpicciolire (scale)
  • Ruotare (rotate)
  • Inclinare (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é:

  1. Le abbiamo già incontrate come colpevole del bug transformed-ancestor.
  2. Sono il completamento naturale di top/left — entrambi spostano elementi "sopra al layout" senza riflusso.
  3. Il pattern più comune (centraggio con translate(-50%, -50%)) si usa insieme a position: fixed.

Transform e animazioni

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'Immagine

Prima 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:

  • Il testo dentro scala insieme al box (non si ricalcola il line-wrapping).
  • Gli algoritmi di layout non se ne accorgono. Il box continua a occupare il suo spazio originario nel flusso.
  • Non vengono ricalcolate la posizione dei fratelli, non cambia l'altezza del genitore.
.box {
  /* Forma generale */
  transform: <funzione>(<valore>);
}

/* Esempi */
.b1 { transform: translateX(20px); }
.b2 { transform: scale(1.5); }
.b3 { transform: rotate(45deg); }
.b4 { transform: skewX(10deg); }

HTML di partenza

<div class="box">box</div>

translate

box

scale

box

rotate

box

skew

box

Le transform si applicano come stringa. Possiamo passarne una o più insieme (ci torniamo nella slide sulla composizione).

Perché è così potente questo modello

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 Y

translate 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); }

HTML di partenza

<div class="box">box</div>
translateX: 0px
translateY: 0px
box
.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.

Spostare con top/left o con translate()?

Ora che abbiamo visto entrambi, è il momento di confrontarli. Non sono intercambiabili.

Con top/left

1
2
3
4

Riferimento: il containing block. Percentuali = dimensioni del genitore.

Con translate()

1
2
3
4

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:

  • Piccole sistemazioni tipografiche (nudge di 1-2px) con position: relative
  • Quando l'elemento deve essere anche un containing block per figli absolute
  • Quando volete creare uno stacking context con z-index numerico

Quando usare translate():

  • Centraggio con -50% (vedi slide successiva)
  • Animazioni e transizioni (performance)
  • Spostamenti relativi alle dimensioni dell'elemento (100%)
  • Combinare movimento con rotate/scale

Regola pratica: se state per scrivere un'animazione, usate translate. Se state posizionando un layout statico con offset piccoli, top/left vanno benissimo.

Il Pattern di Centraggio che Tutti Usano

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%);
}

HTML di partenza

<div class="modal">Modale</div>
translateX: 0%
translateY: 0%
modale
.modal {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(0%, 0%);
}

Cosa succede passo-passo:

  1. top: 50% sposta il bordo superiore dell'elemento al 50% del viewport. L'elemento è troppo in basso.
  2. left: 50% fa lo stesso sull'asse X. L'elemento è troppo a destra.
  3. 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.

Piccolo gotcha

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 Effort

scale 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 */

HTML di partenza

<div class="box">Ciao!</div>

Il valore è un moltiplicatore senza unità (come line-height): 2 = doppio, 0.5 = metà, 1 = invariato, 0 = invisibile.

scaleX: 1
scaleY: 1
Ciao!
.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:

  • Un'animazione di pop (scale 1 → 1.1 → 1) fa pulsare TV vintage tutto un elemento, testo incluso: effetto coerente.
  • Un'animazione di ingrandimento modale non ricalcola la dimensione dei caratteri a ogni frame: liscio e veloce.

Le librerie di animazione

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) */

HTML di partenza

<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.

rotate: 0deg
skewX: 0deg
box
.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 */

HTML di partenza

<div class="box">box</div>
rotate: 0deg
origin:
box
.box {
  transform-origin: center;
  transform: rotate(0deg);
}

Effetto visibile:

  • Ruotando con origin center → l'elemento gira in place.
  • Ruotando con origin bottom → l'elemento "dondola" sul piede.
  • Ruotando con origin top right → l'elemento "cade" come una porta.
  • Scalando con origin 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.

Combinare Più Transform: l'Ordine Conta

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); }

HTML di partenza

<div class="box">box</div>
ordine:
rotate: 45deg
box
.box {
  transform: rotate(45deg) translateX(80px);
}

Risultati diversi!

  • A 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°.
  • B 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 inline

Un'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):

  1. 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);
    }
  2. Passare a Flexbox o Grid: se l'elemento fa parte di un layout strutturato, metterlo in un contenitore flex o grid risolve tutto automaticamente — i figli di flex/grid non sono mai inline.
  3. display: block: se non vi serve che l'elemento stia in-line con il testo.

Inline puro

Why Hello there!

La transform viene ignorata: il testo resta identico.

Con inline-block

Why Hello there!

Appena cambia display, la rotazione prende effetto.

Nota

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.

Il Nuovo Modo Moderno: Proprietà Singole

⏩ Slide bonus — tempo permettendo

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;
}

HTML di partenza

<article class="card">card</article>
translate Y: 0px
rotate: 0deg
scale: 1
card
.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; }
  • Animazioni indipendenti su transizioni e keyframe: ogni proprietà ha la sua timing. Un elemento può ruotare in 2s mentre scala in 500ms, senza dover calcolare i valori intermedi.

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).

Provate voi!

Il Laboratorio di Transform

Aprite CodePen e incollate il codice. Tre box, tre missioni. Usate DevTools per modificare le proprietà transform in tempo reale e osservate cosa succede.

HTML di partenza

<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>

CSS di partenza

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 */
}

Cosa esplorare

  • Missione 1: aggiungete transform: translate(-50%, -50%) alla .centered-card. Il centro dell'elemento ora coincide con il centro del wrapper, indipendentemente dalle dimensioni del testo.
  • Missione 2: aggiungete transform: rotate(15deg) al :hover. Passate il mouse: la transizione è gratis grazie al transition sulla .card.
  • Missione 3: provate diverse composizioni al hover, in ordini diversi:
    • transform: scale(1.1) rotate(5deg) translateY(-10px);
    • transform: translateY(-10px) rotate(5deg) scale(1.1);
    • Notate differenze? (Suggerimento: qui sì, piccole; in animazioni complesse sono drammatiche.)
  • Sfida bonus: cambiate transform-origin sulla Box 3 a top center e rifate il rotate. Come cambia?
  • Sfida bonus 2: riscrivete la Missione 3 usando le proprietà individuali (translate, rotate, scale autonome). L'effetto al hover funziona ancora? (Sì, e il CSS è più modulare.)

Riepilogo: il Toolkit delle 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.

Il Top Layer: Sopra Qualunque Stacking Context

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:

  • L'elemento <dialog> aperto con dialog.showModal()
  • Elementi con l'API Popover (<div popover> + popovertarget)
  • Elementi in modalità fullscreen (video, giochi, applicazioni)

Perché questa differenza è importante?

Una modale costruita con div + position: fixed + z-index: 9999 può essere ancora "battuta" da:

  • Un menu <select> nativo (che usa il top layer del sistema operativo)
  • Un altro overlay che arriva dopo nel DOM con z-index più alto
  • Un componente di terze parti che non conosce i vostri numeri

Il pattern moderno e accessibile:

const dialog = document.querySelector('dialog');
dialog.showModal();
dialog::backdrop {
  background: rgba(0, 0, 0, 0.6);
}

HTML di partenza

<main class="page">...contenuto della pagina...</main>
<dialog open>
  <h3>Conferma</h3>
  <p>Testo della modale</p>
</dialog>

div + z-index

Modale custom

Funziona, ma resta dentro le normali regole di layering CSS.

<dialog> nel top layer

<dialog>

La modale nativa sale sopra tutto il resto senza inseguire i numeri.

<dialog> + showModal() gestisce automaticamente:

  • Posizionamento nel top layer (sopra qualunque stacking context)
  • inert sul resto del documento
  • Focus trap dentro la modale
  • Chiusura con Esc
  • ::backdrop come overlay nativo

L'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>.

Esercizio

Esercizio Finale: Costruire una Modale Professionale

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.

L1 — Strato base (solo CSS, tutti devono arrivarci)

  • Overlay a schermo intero: uno strato semi-trasparente (rgba(0,0,0,0.6)) copre tutta la pagina, anche durante lo scroll.
  • Modale centrata: al centro esatto dello schermo, max 500px di larghezza.
  • Body scrollabile: se il contenuto è troppo lungo, la modale non deve uscire dal viewport. Struttura interna: .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.

L2 — Layering corretto

  • Layering esplicito: la modale deve stare sopra ogni contenuto della pagina. Aggiungete isolation: isolate all'overlay. Controllate che nessun antenato dell'overlay abbia una transform (romperebbe position: fixed della modale).

L3 — Accessibilità statica

  • Bottone close accessibile: l'icona × deve avere un testo .visually-hidden per gli screen reader ("Chiudi finestra di dialogo").
  • ARIA: aggiungete aria-modal="true" alla modale. Aggiungete inert al <main class="page"> dietro la modale.

L4 — Accessibilità dinamica (bonus, richiede JavaScript)

I seguenti quattro requisiti non si implementano in CSS; vanno oltre lo scope di questa lezione. Per chi vuole esplorarli a casa:

  • Focus trap: il Tab non deve uscire dalla modale mentre è aperta.
  • Focus iniziale su un elemento interno alla modale all'apertura.
  • Chiusura con il tasto Esc.
  • Ritorno del focus all'elemento che aveva aperto la modale.

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().

Starter HTML

<!-- 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>

Suggerimenti

  • L1: Iniziate dall'overlay: 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.
  • L2: Aggiungete isolation: isolate all'overlay. Se la modale non si centra, controllate che .overlay non abbia una transform.
  • L3: Aggiungete inert al <main class="page"> (non serve aria-hidden insieme a inert).

Domande di auto-verifica

  • Se aggiungete un tooltip dentro la modale con position: absolute, a chi si aggancia?
  • Se la pagina dietro ha un header sticky, sta sopra o sotto l'overlay?
  • L'utente può "tabbare" sul contenuto della pagina dietro la modale? (Con inert: no.)
  • La modale resta centrata se l'utente ridimensiona la finestra?

E Adesso Sapete Ripararlo

Le tre idee da portare a casa:

  • Il layout non è una cosa sola. Flow, Flexbox, Grid e Positioned sono algoritmi distinti. Cambiare position significa cambiare quale algoritmo governa un elemento.
  • Tutto è connesso. 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.
  • L'accessibilità non è opzionale. Ogni decisione di positioning o hiding ha effetti sull'ordine di Tab, sugli screen reader, sull'esperienza di chi usa il web diversamente da voi.

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.

1 / 61
Indice slide · ⌘K

Indice slide

⌘K