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: Tornerete a costruire un overlay di questo tipo nella lezione dedicata a <dialog> e all'API popover, dove il browser farà gran parte del lavoro al posto vostro. L'esercizio finale di questa lezione, invece, è un recap che mette insieme position, overflow, hiding e transform su una pagina catalogo.

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.

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.) C'è anche un ponte con la lezione di Grid: lì abbiamo risolto un problema sticky con una ricetta (wrapper interno + align-self: start) senza spiegarla davvero. Oggi finalmente capiremo perché quel trucco serviva, e non ci sembrerà più magia.

Cosa vedremo:

  • position: sticky: il valore che scorre con la pagina e poi si blocca — come un ascensore che si ferma a un piano
  • I tre modi classici in cui sticky fallisce e come diagnosticarli
  • Come nascondere gli elementi
  • Perché ogni tecnica di hiding ha implicazioni per l'accessibilità e i lettori di schermo

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 magnete montato sull'elemento e attratto dal bordo del viewport. Finché il bordo è lontano, l'elemento scorre normalmente insieme al contenuto, come relative. Quando arriva alla distanza indicata da top, il magnete si aggancia e l'elemento resta lì, quasi come fosse fixed. Ma l'aggancio ha un limite: lo sticky non può uscire dal suo container, quindi quando il genitore finisce, anche lui viene trascinato via.

Mentre scrollate, l'elemento si comporta come relative. Arrivato al bordo, "si attacca" e inizia a comportarsi come fixed finché il suo genitore non sparisce dalla vista.

header {
  position: sticky;
  top: 0;  /* ⚠️ Serve SEMPRE un offset! */
}

Due cose fondamentali:

  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.

Sticky Troubleshooting: I 3 Bug Classici

Quando sticky non funziona, quasi mai il problema è position: sticky in sé. Di solito state sbagliando il contesto intorno all'elemento.

Bug 1: un antenato con overflow. Sticky si attacca al primo scroll container che incontra risalendo il DOM. Se un genitore, nonno o bisnonno ha overflow: auto, scroll o hidden, il riferimento non è più il viewport ma quel container. Risultato: lo sticky si incolla dentro quel box, oppure sembra non fare nulla se quel box non scrolla davvero.

Bug 2: non c'è abbastanza spazio per muoversi. Sticky ha bisogno di un tratto di strada. Se il container è alto quanto lo sticky, o quasi, non esiste una fase visibile in cui l'elemento scorre e poi si attacca. In pratica sembra rotto, ma in realtà non ha spazio di manovra.

Bug 3: Grid o Flex lo stirano. In Grid e Flex gli item vengono spesso stretchati sull'asse trasversale. Se la sidebar sticky occupa già tutta l'altezza della sua cella, non può "viaggiare". Il fix classico è aggiungere un wrapper interno e usare align-self: start sul wrapper, così la cella smette di stirarsi e lo sticky torna libero di muoversi.

Checklist mentale: se sticky non si attacca, controllate in quest'ordine gli overflow degli antenati, lo spazio disponibile nel container, e lo stretch implicito di Grid/Flex. Nove volte su dieci il problema è uno di questi tre.

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 (una classe con un ruleset specifico) 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;
}
<a href="/blog/accessibilita">
  Leggi di più
  <span class="visually-hidden">
    sull'articolo Accessibilità web
  </span>
</a>

Qui .visually-hidden è spesso preferibile a aria-label. Il nome accessibile del link contiene già il testo visibile — "Leggi di più" — e lo estende con il contesto, invece di sostituirlo con una frase diversa. Questo aiuta anche chi usa software di riconoscimento vocale: può dire "Leggi di più", perché quelle parole fanno davvero parte del nome accessibile. aria-label resta utilissimo soprattutto per controlli senza testo visibile, come un bottone con sola icona.

aria-hidden="true" — il contrario di visually-hidden: visibile agli occhi, invisibile agli screen reader:

<button>
  <span aria-hidden="true">🔍</span>
  Cerca
</button>

Qui la lente è solo decorativa: il significato vero è già nel testo "Cerca". Con aria-hidden="true" dite allo screen reader di ignorare quel pezzetto visibile, così il controllo viene annunciato semplicemente come "Cerca". Questo è il caso giusto per aria-hidden: un frammento visibile, non focusable, che non aggiunge informazione utile.

Trappola

aria-hidden non rimuove gli elementi dall'ordine di Tab. Quindi usatelo solo su pezzi decorativi non interattivi. Se lo mettete su un link, un bottone, o un contenitore con elementi focusable dentro, create un oggetto raggiungibile da tastiera ma invisibile ai lettori di schermo.

inert — l'attributo moderno (2023) che risolve il problema:

<!-- ✅ Corretto: inert da solo basta -->
<p inert>
  This paragraph contains <a href="/">a link</a>.
</p>

<!-- ⚠️ Didattico (per confronto, non il pattern raccomandato) -->
<p inert aria-hidden="true">...</p>

inert rimuove l'elemento e tutti i suoi discendenti dall'interazione: niente focus, niente click, niente screen reader. Supportato da tutti i browser moderni; è il modo giusto per disattivare completamente una sezione (es. contenuto dietro una modale aperta).

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

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 (es. aggiungere contesto a “Leggi di più”)
aria-hidden="true" Volete che gli screen reader ignorino un elemento visibile ma decorativo (icone, separatori, duplicati)
inert Volete disattivare tutto di un sottoalbero (background di una modale aperta)

Regola d'oro per l'accessibilità: prima di nascondere qualcosa, chiedetevi "per chi lo voglio nascondere: la vista, la tastiera, lo screen reader, o tutti?". La risposta determina il metodo.

Prossima sezione: il bonus della lezione. Le transform, lo strumento per spostare, scalare e ruotare gli elementi senza toccare il layout. Il complemento perfetto di tutto quello che abbiamo visto finora.

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
Reflow dei fratelli No (se relative o absolute) No
Richiede position settato? Sì (relative/absolute/fixed) No, funziona su qualsiasi elemento (tranne inline in flow)
Crea stacking context? Sì (con z-index) Sì, sempre
Performance animazioni Scarsa (ri-layout) Ottima (GPU, niente layout)
Crea containing block per fixed? No Sì, attenzione

Quando usare top/left:

  • 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. Il testo dentro scala visivamente, la dimensione dei caratteri effettiva non cambia. Non può essere diversamente: sarebbe un ri-layout.

A volte questo "difetto" è una feature:

Pensate a un'animazione di tipo "pop" (scale 1 → 1.1 → 1).

rotate() e skew() — Le Funzioni "Espressive"

rotate ruota l'elemento attorno al proprio centro:

.box { transform: rotate(45deg); }      /* 45 gradi orari */
.box { transform: rotate(-90deg); }     /* 90 antiorari */
.box { transform: rotate(0.25turn); }   /* = 90deg */
.box { transform: rotate(1turn); }      /* giro completo (360deg) */

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.

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-word {
  transform: rotate(-10deg);  /* non fa nulla! */
}
<p>
  Tu mi fai <span class="inline-word">girar</span> come fossi una bambola
</p>

Perché? Gli inline element sono progettati per "andare con il flusso" del testo, con il minimo disturbo possibile. Distorcerli romperebbe il line-wrapping. Il browser semplicemente ignora la transform.

Le soluzioni (in ordine di preferenza):

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

Tu mi fai girar come fossi una bambola

La transform viene ignorata: il testo resta identico.

Con inline-block

Tu mi fai girar come fossi una bambola

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 Metodo Moderno: Proprietà Singole

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.

Esercizio

Esercizio Finale: Bacheca Prodotti — Recap della Lezione

Costruirete una mini-pagina catalogo in cui ogni pezzo motiva un concetto visto a lezione. Solo HTML + CSS, niente JavaScript. Usate CodePen.

Cosa ripassate

FamigliaConcetti messi in pratica
Position i 4 valori "attivi": relative, absolute, fixed, sticky (più static come default su <main>, per contrasto)
Overflow overflow-x: auto sul carosello + overflow: clip per contenere lo zoom hover
Hiding .visually-hidden (visibile solo agli screen reader) + aria-hidden="true" (visibile solo agli occhi)
Transform rotate sul badge con transform-origin all'angolo + chain rotate + scale sull'hover del badge (l'ordine conta)

Cosa costruite

┌─────────────────────────────────┐
│ [STICKY HEADER · logo · menu]    │ ← .page-header → position: sticky
├──────────────────────────────────┤
│ <main class="page">              │   <main> resta static (default)
│  ┌───────┐ ┌───────┐ ┌───────┐ →│ ← .gallery → overflow-x: auto
│  │ IMG   │ │ IMG   │ │ IMG   │  │   .card-image → overflow: clip
│  │[-30%]◄┼─┤badge  │ │       │  │   .product-card → position: relative
│  └───────┘ └───────┘ └───────┘  │   .card-badge → position: absolute
│  hover badge: rotate+scale       │                  + rotate(-8deg)
├──────────────────────────────────┤
│ Footer                           │
└──────────────────────────────[↑]─┘ ← .fab → position: fixed
                                       button × · ♥ con .visually-hidden
                                       ★ decorativo con aria-hidden="true"

I quattro livelli

Ogni livello pratica una famiglia di concetti con 2-3 step concreti. Chi finisce L1 ha consegnato un esercizio valido; chi finisce L4 ha un recap completo.

L1 — Positioning: i quattro valori in una pagina

  1. .page-headerposition: sticky; top: 0;
  2. .product-cardposition: relative; (containing block per il badge)
  3. .card-badgeposition: absolute; top: 8px; right: 8px;
  4. .fab (bottone "↑ torna su" in basso a destra) → position: fixed; bottom: 24px; right: 24px;
  5. <main class="page"> → niente position. È static, il default. Lo lasciate così per contrasto.

L2 — Overflow: auto e clip non sono sinonimi

  1. .gallery (la <ul> con le card) → display: flex; overflow-x: auto;. Le card non vanno a capo: scrollano in orizzontale.
  2. .card-imageoverflow: clip;. Sull'<img>:hover mettete transform: scale(1.06) con una transition. Il bordo arrotondato ritaglia lo zoom.
  3. Domanda guida (rispondete nel commento CSS): perché qui clip è preferibile a hidden? Cosa significa per il sistema di sticky/transform?

L3 — Hiding accessibile: due tecniche, scopi opposti

  1. .visually-hidden sui bottoni icon-only "×" (chiudi banner) e "♥" (wishlist): aggiungete <span class="visually-hidden">Aggiungi ai preferiti</span> accanto all'icona. Definite voi la classe con il pattern visto a lezione (clip-based).
  2. aria-hidden="true" sulla stella decorativa ★ accanto al titolo "Bestseller": gli occhi la vedono, gli screen reader la ignorano.
  3. Spiegate nel commento CSS perché sono complementari: .visually-hidden nasconde agli occhi, aria-hidden nasconde agli screen reader.

L4 — Transform: una statica, una in chain (con transform-origin)

Tutto il transform vive sul badge. Stato base e stato hover della card — ma il transform che cambia è sempre quello del badge.

  1. .card-badge (stato base) → transform: rotate(-8deg); con transform-origin: top right; e transition: transform .2s. L'origin "cuce" il badge all'angolo della card invece di farlo ruotare attorno al proprio centro: provate a togliere la riga transform-origin e vedete come il badge "scivola" dentro la card.
  2. .product-card:hover .card-badgetransform: rotate(-12deg) scale(1.1);. Due funzioni in chain — l'ordine conta (recap Slide 54): scale dopo rotate ingrandisce nella direzione già ruotata.
  3. Domanda bonus (rispondete nel commento CSS): qui il transform è sul badge, che non ha figli con position: fixed — quindi non rompe niente. Ma se invece mettessi transform sulla .product-card e ci avessi dentro un elemento con position: fixed, cosa succederebbe? (Riferimento Slide 26.)

Codice di partenza

<header class="page-header">
  <strong>Bacheca</strong>
  <nav>Home · Catalogo · Account</nav>
  <button class="close-banner">
    <span aria-hidden="true">×</span>
    <span class="visually-hidden">Chiudi banner promozionale</span>
  </button>
</header>

<main class="page">
  <h1>I bestseller della settimana <span aria-hidden="true">★</span></h1>

  <ul class="gallery">
    <li class="product-card">
      <div class="card-image">
        <img src="https://picsum.photos/seed/a/400/300" alt="Sedia in legno chiaro">
      </div>
      <span class="card-badge">-30%</span>
      <h3>Sedia Linnmon</h3>
      <p>€ 89</p>
      <button class="wishlist">
        <span aria-hidden="true">♥</span>
        <span class="visually-hidden">Aggiungi ai preferiti</span>
      </button>
    </li>
    <!-- Duplicate la card 4-5 volte per testare il carosello -->
  </ul>

  <p>Aggiungete tanto contenuto qui sotto per testare lo scroll verticale e il comportamento sticky dell'header.</p>
</main>

<button class="fab">
  <span aria-hidden="true">↑</span>
  <span class="visually-hidden">Torna su</span>
</button>

Domande di auto-verifica

  1. Il FAB sta sopra o sotto l'header sticky quando si scrolla? E se aggiungete transform a un antenato del FAB, cosa succede al suo position: fixed?
  2. Perché overflow: clip su .card-image invece di overflow: hidden? Cosa cambia per il sistema di scroll/sticky?
  3. Se rimuovete position: relative da .product-card, dove finisce il badge?
  4. Cosa "vede" uno screen reader sul bottone "♥"? E sulla stella decorativa ★?
  5. Cosa succede al badge se mettete overflow: clip sulla .product-card invece che sulla .card-image?
Mostra soluzione

Una soluzione possibile. Confrontate con la vostra: se avete fatto scelte diverse ma che funzionano, ottimo — qui interessa il perché, non l'identità byte-per-byte.

/* Reset minimo */
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; }

/* L1 — Positioning */

.page-header {
  position: sticky;          /* scorre con la pagina, poi si blocca */
  top: 0;
  background: #fff;
  border-bottom: 1px solid #ddd;
  padding: 12px 24px;
  display: flex;
  gap: 16px;
  align-items: center;
  z-index: 10;
}

.page {
  /* niente position: è static, il default. Citato per contrasto. */
  padding: 24px;
}

.product-card {
  position: relative;        /* containing block per il badge */
  list-style: none;
  width: 240px;
  flex-shrink: 0;            /* non si rimpicciolisce nel flex carosello */
  border: 1px solid #ddd;
  border-radius: 12px;
  padding: 12px;
  background: #fff;
}

.card-badge {
  position: absolute;        /* esce dal flusso, ancorato alla card */
  top: 8px;
  right: 8px;
  background: #e11d48;
  color: #fff;
  padding: 4px 10px;
  border-radius: 4px;
  font-weight: 700;
  transform: rotate(-8deg);  /* L4 — stato base */
  transform-origin: top right; /* L4 — "cuce" il badge all'angolo;
                                  senza, il rotate ruoterebbe attorno al centro
                                  e il badge "scivolerebbe" dentro la card */
  transition: transform .2s;
}

.fab {
  position: fixed;           /* ancorato al viewport */
  bottom: 24px;
  right: 24px;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  border: none;
  background: #1e40af;
  color: #fff;
  font-size: 24px;
  cursor: pointer;
  z-index: 20;
}

/* L2 — Overflow */

.gallery {
  display: flex;
  gap: 16px;
  overflow-x: auto;          /* scrollbar orizzontale solo se serve */
  padding: 8px 0;
  list-style: none;
  margin: 0;
}

.card-image {
  overflow: clip;            /* taglia lo zoom SENZA creare scroll container.
                                Con `hidden` creeremmo uno scroll container che
                                interferirebbe con sticky/transform discendenti.
                                `clip` taglia visivamente e basta. */
  border-radius: 8px;
}

.card-image img {
  width: 100%;
  height: auto;
  display: block;
  transition: transform .3s;
}

.product-card:hover .card-image img {
  transform: scale(1.06);    /* lo zoom esce dai bordi → clippato da overflow: clip */
}

/* L3 — Hiding accessibile */

.visually-hidden {
  /* Visibile agli SCREEN READER, invisibile agli OCCHI.
     Pattern clip-based standard (più robusto di display: none,
     che invece nasconderebbe a tutti). */
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* La ★ accanto al titolo usa solo aria-hidden="true" nell'HTML:
   è il CONTRARIO COMPLEMENTARE di .visually-hidden — visibile agli occhi,
   muta agli screen reader. Niente CSS necessario. */

.wishlist,
.close-banner {
  background: transparent;
  border: none;
  font-size: 20px;
  cursor: pointer;
}

/* L4 — Transform: chain di due funzioni sull'hover del badge */

.product-card:hover .card-badge {
  transform: rotate(-12deg) scale(1.1);
  /* Chain di due funzioni — l'ordine conta (recap Slide 54).
     scale DOPO rotate: il badge prima ruota a -12deg, poi viene ingrandito
     nella direzione già ruotata. Provate a invertire — l'effetto cambia.

     Nota Slide 26: qui il transform è sul badge, che non ha figli con
     position: fixed — quindi non rompe niente. Se invece mettessi
     transform sulla .product-card e ci avessi dentro un fixed, quel
     fixed sceglierebbe la card come containing block invece del viewport. */
}

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.

Grazie per l'attenzione..

1 / 61
Indice slide · ⌘K

Indice slide

⌘K