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

Guida interattiva a

CSS Responsive

Adattarsi a ogni schermo, ogni utente, ogni contesto

Adattato da Josh W. Comeau — css-for-js.dev

Un Mondo di Differenze

Una delle sfide più grandi nella costruzione di interfacce web è che devono funzionare in una gamma enorme di situazioni diverse. Pensate a quante cose cambiano da un utente all'altro:

  • Dimensione e orientamento dello schermo — dal telefono verticale al monitor ultrawide
  • Tipo di dispositivo — telefono, tablet, laptop, desktop, smartwatch…
  • Meccanismo di input — mouse, trackpad, dito, tastiera, dettatura vocale
  • Preferenze dell'utente — tema chiaro/scuro, contrasto elevato, movimento ridotto
  • Livello di zoom / dimensione del font di default
  • Prestazioni del dispositivo e della rete

E queste sono solo le principali!

Per aiutarci a costruire interfacce flessibili che affrontino queste sfide, il CSS offre strumenti molto potenti.

Cosa imparerete in questa lezione:

  • Le media query, lo strumento principale per adattare il layout
  • Come costruire interfacce fluide con vincoli dinamici
  • Funzioni CSS moderne come clamp(), min() e max()
  • I container query per componenti che si adattano al proprio spazio
  • I feature query per gestire il supporto progressivo dei browser

Dal Web Fisso al Responsive

Nel 1996 uscì il film Space Jam. Il sito web creato per promuoverlo è ancora online, identico dopo quasi 30 anni — una testimonianza della retrocompatibilità del web.

Screenshot of an old website, showing a number of icons and a starry background

A quei tempi era ragionevole assumere che tutti guardassero il vostro sito su un monitor desktop a 640×480 pixel. Le dimensioni erano fisse: il sito di Space Jam usava tabelle HTML larghe 500px. Era come stampare un volantino — nulla di dinamico o adattabile.

Poi arrivò l'iPhone.

Per la prima volta, i siti web dovevano funzionare anche su schermi piccoli. La prima soluzione fu il design adattivo (adaptive design): il server inviava pagine HTML diverse a seconda del dispositivo. Chi visitava facebook.com da telefono veniva reindirizzato a m.facebook.com, un sito completamente separato.

The New York Times website on an iPhone, but impossibly small / hard to read

Costruire due versioni di ogni sito era un lavoro enorme. E quando Apple rilasciò l'iPad, a metà strada tra telefono e desktop, fu chiaro che serviva un nuovo approccio.

Il design responsive (responsive design) è l'idea che tutti i dispositivi ricevano lo stesso HTML, ma quel documento si adatti grazie al CSS. Un solo file HTML che funziona ovunque.

Adaptive vs Responsive

Adaptive = HTML separati per ogni dispositivo (più lavoro, meno complessità CSS). Responsive = stesso HTML, CSS flessibile (meno lavoro, più complessità CSS). Oggi il responsive è lo standard.

Pixel Hardware e Pixel CSS

Quasi tutti i telefoni moderni hanno display ad alta densità (High DPI o, nel marketing Apple, display "Retina"). Un iPhone 15 ha una risoluzione nativa di 1179×2556 pixel — più della maggior parte dei monitor desktop!

Se il browser usasse quella risoluzione letteralmente, il testo sarebbe illeggibile. Per questo esiste il device pixel ratio: il rapporto tra i pixel fisici del display (LED) e i pixel "teorici" che usiamo nel CSS.

Su un iPhone tipico, questo rapporto è 3. Significa che una lunghezza di 10px nel CSS corrisponde a 30 pixel fisici. Ogni pixel software corrisponde a 9 pixel hardware (3×3):

A 3x3 grid showing the relationship between software and hardware pixels

La conversione avviene "sotto il cofano". Nel CSS, lavorate solo con i pixel software — ed è tutto ciò che vi serve sapere.

Immagini ad alta densità

Questo è importante per le immagini: su display ad alta densità, un'immagine 200×200 pixel apparirà sfocata se visualizzata a 200×200 CSS pixel. Per immagini nitide servono file a risoluzione doppia o tripla. Vedremo come gestirlo più avanti nel corso.

Il Meta Tag Viewport

Per far funzionare correttamente le pagine responsive su mobile, è obbligatorio inserire questo tag nel <head> del vostro HTML:

<meta name="viewport" content="width=device-width, initial-scale=1.0">

Questo tag è incluso automaticamente in ogni template e boilerplate moderno. Ma cosa fa esattamente?

Il problema storico

Quando uscì l'iPhone, i siti mobile non esistevano ancora. Safari rendeva ogni pagina come se il browser fosse largo 980px, e poi la ridimensionava per farla entrare nei 320px dello schermo. L'utente vedeva una versione microscopica dell'intera pagina, e doveva fare pinch-to-zoom per leggere il contenuto.

La soluzione

  • width=device-width → dice al browser di usare la larghezza reale del dispositivo (es. 320px invece di 980px)
  • initial-scale=1 → dice al browser di partire con zoom a 1x

Senza il meta tag: il browser simula un viewport largo 980px e rimpicciolisce tutto.
Con il meta tag: il browser usa la larghezza reale dello schermo. Il CSS responsive funziona come previsto.

Attenzione — primo errore da controllare

Se dimenticate questo tag, tutte le vostre media query saranno inutili su mobile. Il browser penserà di essere largo 980px e non attiverà mai i breakpoint per schermi piccoli. È il primo errore da controllare quando il responsive "non funziona" su telefono.

Testare su Mobile

Uno dei modi migliori per ridurre i bug è controllare regolarmente il vostro sito su dispositivi mobili. Non basta sviluppare su desktop e sperare che funzioni!

Lo strumento più immediato: DevTools del browser

Tutti i browser moderni includono una modalità responsive integrata nei DevTools:

  1. Aprite i DevTools (F12 o Ctrl+Shift+I / Cmd+Opt+I)
  2. Cliccate sull'icona del dispositivo nella barra degli strumenti
  3. Scegliete un dispositivo dal menu a tendina, oppure trascinate i bordi per ridimensionare liberamente

Questo vi permette di simulare diverse larghezze di schermo senza uscire dal browser. Vedrete le media query attivarsi in tempo reale mentre ridimensionate.

Per test più accurati: dispositivi reali

La simulazione nei DevTools è ottima per lo sviluppo quotidiano, ma non cattura tutto. Per risultati affidabili:

  • Usate il vostro telefono personale per test rapidi
  • Un dispositivo Android economico è utile come riferimento per prestazioni "reali"
  • Servizi come BrowserStack permettono di testare su dispositivi reali da remoto
  • Simulator di XCode (solo MacOS) e Android Studio (Mac e Win) permettono di emulare device completi

La simulazione non è tutto

La simulazione del browser non è perfetta: non riproduce le prestazioni reali del dispositivo, le gesture touch, o il comportamento specifico di Safari su iOS. Testate sempre su almeno un dispositivo reale prima di pubblicare.

Provate voi!

Esplorate la Modalità Responsive

Aprite i DevTools del browser su un sito a vostra scelta e attivate la modalità responsive. Osservate come cambia il layout al variare della larghezza!

Risultato atteso

  • Il sito si adatta man mano che restringete la larghezza
  • A certi punti precisi (breakpoint), il layout cambia drasticamente
  • Elementi come le barre di navigazione si trasformano o scompaiono

Cosa esplorare

  • Aprite un sito noto (es. github.com, wikipedia.org) e attivate la modalità responsive dei DevTools
  • Restringete la larghezza gradualmente: a quali punti vedete il layout cambiare?
  • Provate il menu a tendina dei dispositivi: scegliete "iPhone 14" e poi "iPad Air" — cosa cambia?
  • Osservate la larghezza indicata in alto: quei numeri sono i pixel CSS, non i pixel hardware
  • Cercate elementi che scompaiono su mobile (sidebar, colonne secondarie) — dove vanno?

Le Unità di Misura nel CSS

Quando assegnate dimensioni nel CSS — font, margini, padding, larghezze — dovete specificare un'unità di misura. Le tre unità più importanti da conoscere sono:

UnitàNomeRiferimento
pxPixelDimensione fissa
emEmRelativa al font-size dell'elemento (o del genitore)
remRoot emRelativa al font-size dell'elemento radice (<html>)
.titolo {
  font-size: 32px;   /* Fisso: sempre 32px */
}

.paragrafo {
  font-size: 1.5em;  /* 1.5 volte il font-size del genitore */
}

.bottone {
  font-size: 1rem;   /* 1 volta il font-size della radice (16px di default) */
}

Capire la differenza tra queste unità è fondamentale per il design responsive: la scelta dell'unità determina come e se i vostri layout si adattano alle preferenze dell'utente.

Pixel — L'Unità Fissa

Il pixel (px) è l'unità più intuitiva: un valore in pixel ha sempre la stessa dimensione visiva, indipendentemente dal contesto.

.bordo {
  border: 2px solid black;     /* Bordo di 2px */
  box-shadow: 0 4px 8px gray;  /* Ombra di 4px e 8px */
}

.titolo {
  font-size: 24px;   /* Sempre esattamente 24px */
  margin-bottom: 8px;
}

Quando usare i pixel

  • Bordi (border: 1px solid)
  • Ombre (box-shadow)
  • Piccoli spazi fissi che non devono cambiare

Il problema

I pixel non si adattano alle preferenze dell'utente. Se un utente con problemi di vista aumenta la dimensione del font predefinita nel browser (ad esempio da 16px a 24px), un testo scritto in px rimane identico — non cresce.

Questo è un problema di accessibilità: l'utente ha chiesto testo più grande, ma il vostro sito ignora la richiesta.

Attenzione — accessibilità

I pixel sono perfetti per dettagli decorativi (bordi, ombre, piccoli gap). Ma per dimensioni del testo, padding e margini importanti, ci sono unità migliori — come em e rem.

em — L'Unità Relativa al Genitore

L'unità em (dal termine tipografico inglese per la larghezza della lettera "M") è un'unità relativa: il suo valore dipende dal font-size dell'elemento corrente. Quando usata per impostare il font-size stesso, si riferisce al font-size del genitore (parent).

.genitore {
  font-size: 20px;
}

.figlio {
  font-size: 1.5em;    /* 1.5 × 20px = 30px */
  padding: 1em;         /* 1 × 30px = 30px (usa il font-size dell'elemento) */
}

Il vantaggio: se usate em per padding e margini, questi crescono proporzionalmente con il testo. Un bottone con padding: 0.5em 1em mantiene le sue proporzioni a qualsiasi dimensione del font.

Il problema — la composizione (compounding)

Quando annidate elementi con em, i valori si moltiplicano tra loro:

.livello-1 {
  font-size: 1.5em;   /* 1.5 × 16px = 24px */
}

.livello-2 {
  font-size: 1.5em;   /* 1.5 × 24px = 36px */
}

.livello-3 {
  font-size: 1.5em;   /* 1.5 × 36px = 54px! */
}

Con tre livelli di annidamento, 1.5em diventa 1.5 × 1.5 × 1.5 = 3.375 volte la dimensione originale! Questo effetto a catena rende em imprevedibile in strutture complesse.

Quando usare em

L'unità em è utile quando volete che padding e margini scalino insieme al testo dell'elemento (es. bottoni, badge). Evitate di usarla per il font-size in strutture annidate — il compounding vi sorprenderà.

rem — L'Unità Relativa alla Radice

L'unità rem (root em, cioè "em della radice") funziona come em, ma con una differenza cruciale: è sempre relativa al font-size dell'elemento <html>, mai al genitore.

Per default, il font-size della radice è 16px in tutti i browser. Quindi:

ValoreCalcoloRisultato
1rem1 × 16px16px
1.5rem1.5 × 16px24px
2rem2 × 16px32px
0.875rem0.875 × 16px14px

Nessuna composizione: a differenza di em, annidare elementi non cambia nulla:

.livello-1 { font-size: 1.5rem; }  /* 24px */
.livello-2 { font-size: 1.5rem; }  /* 24px — sempre! */
.livello-3 { font-size: 1.5rem; }  /* 24px — sempre! */

Il punto chiave — accessibilità

Alcuni utenti modificano la dimensione del font predefinita del browser nelle impostazioni (ad esempio, persone con problemi di vista). Circa il 3% degli utenti cambia questa impostazione — un numero significativo.

Quando un utente imposta il font predefinito a 24px invece di 16px:

/* Con font predefinito del browser a 24px: */
.testo-rem {
  font-size: 1rem;    /* → 24px ✓ Rispetta la scelta dell'utente */
}
.testo-px {
  font-size: 16px;    /* → 16px ✗ Ignora la scelta dell'utente */
}

Usare rem per le dimensioni del testo significa rispettare le preferenze di accessibilità dei vostri utenti.

Il 3% conta

Circa il 3% degli utenti cambia la dimensione del font predefinita del browser. Su un sito con 100.000 visitatori mensili, sono 3.000 persone che vedranno il vostro testo troppo piccolo se usate px. L'unità rem è la scelta consigliata per font-size, padding e margini.

Provate voi!

Confrontate px, em e rem

Aprite CodePen e create elementi con dimensioni in px, em e rem. Osservate come si comportano quando cambiate il font-size della radice e quando annidate gli elementi!

Risultato atteso

  • I testi in px restano sempre della stessa dimensione
  • I testi in rem crescono e si riducono con il font-size della radice
  • I testi in em si moltiplicano ad ogni livello di annidamento

Codice di partenza

<div class="demo">
  <h2 class="titolo-px">Titolo in px (24px)</h2>
  <h2 class="titolo-rem">Titolo in rem (1.5rem)</h2>

  <div class="annidamento">
    <p class="em-livello">Livello 1 (1.2em)</p>
    <div class="annidamento">
      <p class="em-livello">Livello 2 (1.2em)</p>
      <div class="annidamento">
        <p class="em-livello">Livello 3 (1.2em)</p>
      </div>
    </div>
  </div>

  <div class="bottone-demo">
    <button class="btn btn-piccolo">Bottone piccolo</button>
    <button class="btn btn-grande">Bottone grande</button>
  </div>
</div>
/* Provate a cambiare questo valore: 16px, 20px, 24px */
html {
  font-size: 16px;
}

.demo {
  padding: 1rem;
}

/* Confronto px vs rem */
.titolo-px {
  font-size: 24px;      /* Non cambia mai */
  color: #e74c3c;
}

.titolo-rem {
  font-size: 1.5rem;    /* Cambia con il font-size di html */
  color: #2ecc71;
}

/* Composizione di em */
.annidamento {
  font-size: 1.2em;     /* Si moltiplica ad ogni livello! */
  padding-left: 1em;
  border-left: 2px solid #3498db;
  margin: 8px 0;
}

.em-livello {
  margin: 4px 0;
}

/* Bottoni con em per padding proporzionale */
.btn {
  padding: 0.5em 1em;
  border: 2px solid #333;
  border-radius: 0.25em;
  background: white;
  cursor: pointer;
}

.btn-piccolo {
  font-size: 0.875rem;
}

.btn-grande {
  font-size: 1.5rem;
}

Cosa esplorare

  • Cambiate il valore di html { font-size } da 16px a 24px: quale titolo cresce e quale no?
  • Osservate i tre livelli di annidamento con em: il testo diventa sempre più grande ad ogni livello
  • Guardate i due bottoni: il padding scala proporzionalmente al testo grazie a em
  • Provate a cambiare .annidamento { font-size: 1.2em } in font-size: 1.2rem — la composizione scompare
  • Per simulare un utente con font grande: nelle impostazioni del browser, cercate "Dimensione carattere" e impostatela su "Grande" — poi confrontate i titoli in px e in rem

Le Media Query

Quando si tratta di costruire interfacce responsive, la media query è lo strumento principale nella nostra cassetta degli attrezzi.

Ecco la sintassi base:

.signup-button {
  color: deeppink;
  font-size: 1rem;
}

@media (max-width: 411px) {
  .signup-button {
    font-size: 2rem;
  }
}

La parola chiave @media è una at-rule — un tipo speciale di istruzione CSS che modifica il comportamento delle regole. Avete già incontrato un'altra at-rule: @keyframes, che definisce le sequenze di animazione.

Le media query applicano regole CSS in modo condizionale, in base a una o più condizioni. In questo esempio stiamo dicendo: il selettore .signup-button deve adottare una dichiarazione aggiuntiva quando il viewport è largo 411px o meno (nell'ultima fascia "mobile" del set di breakpoint che useremo nella lezione).

Regole condizionali

Pensate alle media query come a regole condizionali: "se la finestra è più stretta di X, applica anche queste regole". Non è un "o questo o quello" — le regole si sommano.

Le Regole si Sommano

Un punto fondamentale: le media query non creano fogli di stile separati. Le regole si sommano. Quando il viewport è 411px o meno, il bottone del nostro esempio ha entrambe le dichiarazioni attive:

/* Queste regole si sommano quando la condizione è vera: */
color: deeppink;     /* sempre attiva */
font-size: 2rem;     /* attiva sotto 411px */

Non state scegliendo "uno o l'altro" — state aggiungendo regole quando una condizione è soddisfatta.

Attenzione all'ordine!

Le media query non cambiano la specificità. L'unica ragione per cui font-size: 2rem vince su font-size: 1rem è che viene dopo nel codice. Guardate cosa succede se invertiamo l'ordine:

@media (max-width: 411px) {
  .signup-button {
    font-size: 2rem;
  }
}

.signup-button {
  color: deeppink;
  font-size: 1rem;
}

Il bottone non cambia mai dimensione! La dichiarazione font-size: 1rem viene dopo e sovrascrive sempre font-size: 2rem, indipendentemente dalla media query.

Attenzione — ordine delle regole

L'ordine delle regole nel CSS conta sempre. Mettete le media query dopo le regole base che volete sovrascrivere, altrimenti non avranno effetto.

Mobile-First vs Desktop-First

Ci sono due modi distinti di scrivere le media query:

Desktop-first — gli stili di default sono per desktop, poi si sovrascrivono per mobile:

/* Stili di default: desktop */
.signup-button {
  font-size: 2rem;
}

@media (max-width: 411px) {
  .signup-button {
    font-size: 1rem;
  }
}

Mobile-first — gli stili di default sono per mobile, poi si aggiungono per schermi più grandi:

/* Stili di default: mobile */
.signup-button {
  font-size: 1rem;
}

@media (min-width: 412px) {
  .signup-button {
    font-size: 2rem;
  }
}

Il risultato finale è identico. Sono due strade diverse verso la stessa destinazione. Ma il modello mentale è diverso.

Quale scegliere?

La raccomandazione è di essere consistenti: scegliete un approccio e usatelo per tutto il progetto. Se usate min-width ovunque (mobile-first) o max-width ovunque (desktop-first), sarà molto più facile leggere e capire il CSS.

Detto questo, ci sono eccezioni. Se avete un layout che esiste solo su tablet, può avere senso usare una query "esclusiva":

@media (min-width: 768px) and (max-width: 1023px) {
  /* Solo tablet portrait (fascia centrale del nostro set) */
}

px vs rem nelle Media Query

Quando create media query con min-width / max-width, dovreste usare pixel o rem?

La differenza: come avete imparato, l'unità rem equivale a 16px di default, ma può essere modificata — sia dallo sviluppatore, sia dall'utente.

Immaginate un utente con problemi di vista che ha aumentato la dimensione del font base del browser a 32px. Ora ogni rem equivale a 32px invece di 16.

La domanda è: le nostre media query dovrebbero rispettare questa scelta?

Il layout desktop rimane invariato anche con il font ingrandito: testo enorme e poco spazio, sidebar che occupa metà schermo

Con media query in pixel: il layout desktop rimane invariato anche con il font ingrandito. Il risultato è un layout stretto e affollato: la sidebar si espande e occupa metà schermo.

Il browser passa al layout mobile con il font ingrandito: testo grande con molto spazio per respirare

Con media query in rem: il browser passa automaticamente al layout mobile (anche se la finestra è di dimensioni desktop). Il testo grande ha più spazio per respirare, e l'esperienza è decisamente migliore.

Per questo motivo, si raccomanda di usare rem nelle media query nella maggior parte dei casi.

/* Conversione: dividi per 16 */
/* 768px / 16 = 48rem */
@media (min-width: 48rem) {
  /* ... */
}

rem o em?

Potete usare anche em al posto di rem: nelle media query, le due unità funzionano in modo pressoché identico.

Provate voi!

Scrivete le Vostre Prime Media Query

Aprite CodePen e sperimentate con le media query. Ridimensionate il pannello Result per vedere le regole attivarsi!

Risultato atteso

  • Su schermi larghi: testo grande, sfondo chiaro
  • Sotto 768px (mobile + mobile large): testo più piccolo, sfondo colorato

Codice di partenza

<div class="container">
  <h1 class="title">Responsive!</h1>
  <p class="text">Ridimensionate il pannello per vedere cosa cambia.</p>
</div>
.container {
  padding: 32px;
  background: #f0f0f0;
}

.title {
  font-size: 3rem;
  color: #333;
}

.text {
  font-size: 1.2rem;
}

/* Aggiungete le vostre media query qui sotto */

/* @media (max-width: 767px) {
  .title {
    font-size: 1.5rem;
  }
  .container {
    background: #dbeafe;
  }
} */

Cosa esplorare

  • Decommentate la media query e ridimensionate il pannello sotto 768px — cosa cambia?
  • Provate a cambiare max-width: 767px in max-width: 411px — il punto di scatto si sposta sulla soglia "mobile" del set
  • Aggiungete una seconda media query per un'altra soglia (es. max-width: 360px)
  • Provate a riscrivere le stesse regole in stile mobile-first con min-width — ottenete lo stesso risultato?
  • Invertite l'ordine delle regole: mettete la @media prima delle regole base. Cosa succede?

Non Solo Larghezza — Hover e Pointer

Quando parliamo di "media query", pensiamo subito alla larghezza dello schermo. Ma le media query sanno fare molto di più!

Il problema dell'hover su mobile

L'hover è un gesto possibile solo con un dispositivo puntatore come mouse o trackpad. Su mobile, usiamo le dita — e le dita non possono fare hover.

Quando Apple creò Safari per iOS, decise che toccare un elemento interattivo avrebbe attivato lo stato hover. Il risultato:

  • Lo stato hover rimane attivo finché l'utente non tocca qualcos'altro
  • Si può attivare accidentalmente scorrendo la pagina

La soluzione: query di interazione

Il primo istinto potrebbe essere limitare gli stili hover agli schermi grandi. Ma non è corretto: molti utenti desktop restringono la finestra, e esistono touchscreen grandi. L'hover non è una questione di dimensione, è una questione di tipo di input.

@media (hover: hover) and (pointer: fine) {
  .button:hover {
    background: deeppink;
  }
}

Hover e Pointer descrivono capacità diverse:

hover: hoverhover: none
pointer: fineMouse, trackpadStilo
pointer: coarseDito (touchscreen)
  • hover: il dispositivo può muovere il cursore senza cliccare? (mouse sì, dito no)
  • pointer: quanto è preciso il puntatore? fine = mouse/trackpad, coarse = dito

Il browser rileva automaticamente il dispositivo di input in uso, e queste query si aggiornano dinamicamente.

Combinare Condizioni

Nella slide precedente abbiamo usato la parola chiave and per combinare più condizioni:

@media (hover: hover) and (pointer: fine) {
  /* Si applica solo se ENTRAMBE le condizioni sono vere */
}

and funziona come un "e" logico: tutte le condizioni devono essere soddisfatte.

Media Type: screen e print

Un altro uso di and che potreste incontrare:

@media screen and (min-width: 48rem) {
  /* Solo su schermo E con viewport >= 768px (tablet portrait del nostro set) */
}

screen è un media type. Specifica che le regole valgono solo quando il sito è visualizzato su uno schermo. L'alternativa principale è print, che si applica quando la pagina viene stampata su carta o salvata come PDF:

@media print {
  .navigation {
    display: none; /* Nasconde la nav in stampa */
  }
  body {
    font-size: 12pt;
    color: black;
  }
}

Quando usare print

Specificare screen è diventato meno comune negli ultimi anni, perché i browser hanno scelto default più sensati per la stampa. Ma le media query print restano utili per nascondere elementi non rilevanti su carta (navigazione, bottoni, pubblicità).

Rispettare le Preferenze dell'Utente

Le media query possono anche "agganciarsi" alle preferenze personali dell'utente, impostate nel sistema operativo o nel browser.

Tema chiaro / scuro

@media (prefers-color-scheme: dark) {
  body {
    background: #1a1a2e;
    color: #e0e0e0;
  }
}

Questa query rileva se l'utente ha attivato il tema scuro (dark mode) nelle impostazioni del sistema. Potete adattare colori, sfondi e contrasti di conseguenza.

Movimento ridotto

@media (prefers-reduced-motion: reduce) {
  * {
    animation: none !important;
    transition-duration: 0.01ms !important;
  }
}

Alcune persone sono sensibili al movimento: le animazioni possono causare nausea o emicranie. Questa query rileva se l'utente ha richiesto meno movimento e vi permette di disattivare o semplificare le animazioni.

Non è solo una questione di "preferenza" — queste query vi permettono di creare esperienze più sicure e accessibili.

Responsive = adattarsi al contesto completo

Le media query non riguardano solo la dimensione dello schermo. Interazione, preferenze e accessibilità sono altrettanto importanti. Responsive significa adattarsi al contesto completo dell'utente.

I Breakpoint

Per dare struttura al mondo caotico delle dimensioni dei dispositivi, è utile definire una serie di breakpoint (punti di rottura).

Un breakpoint è una larghezza specifica del viewport che ci permette di raggruppare tutti i dispositivi in un piccolo numero di esperienze possibili. Ad esempio, con un breakpoint a 412px, tutti i dispositivi sotto quella soglia ricevono lo stesso layout — chi usa un telefono da 320px e chi ne usa uno da 375px vedranno la stessa cosa.

L'errore comune: inseguire i dispositivi

Molti sviluppatori scelgono breakpoint basandosi sulle risoluzioni dei dispositivi più famosi: "l'iPhone 12 è largo 375px, usiamo quello come breakpoint mobile."

Questo approccio è sbagliato. Le risoluzioni più comuni dovrebbero stare al centro di ogni gruppo, non ai bordi. Un iPhone da 375px dovrebbe essere nello stesso gruppo di un iPhone SE da 320px e di un Android da 390px.

La regola: mettete i breakpoint nelle "zone morte"

I breakpoint vanno posizionati il più lontano possibile dalle risoluzioni reali, in una sorta di terra di nessuno (no-device land). In questo modo, tutti i dispositivi simili condivideranno lo stesso layout.

Visualizzazione dei cluster di dispositivi raggruppati da cerchi, con i breakpoint posizionati negli spazi vuoti tra un gruppo e l'altro

Scegliere i Valori

Le risoluzioni dei dispositivi si presentano in cluster (raggruppamenti). Se disegniamo dei cerchi attorno a questi cluster, sappiamo dove posizionare i breakpoint — negli spazi vuoti tra un gruppo e l'altro.

Ecco un set di breakpoint ragionevole:

RangeCategoriaBreakpoint min-width
0 – 411pxMobile— (stili base)
412 – 767pxMobile large412px / 25.75rem
768 – 1023pxTablet portrait768px / 48rem
1024px – 1279pxTablet landscape / Laptop piccoli1024px / 64rem
1280px+Laptop grandi / Desktop1280px / 80rem

Non esiste un set "perfetto" universale — dipende dal vostro design e dai dispositivi che volete supportare. Ma questo è un buon punto di partenza.

Testate sul dispositivo più piccolo del gruppo

Non preoccupatevi di distinguere tra telefoni "piccoli" (iPhone SE, 320px) e "grandi" (iPhone Pro Max, 430px) — raramente serve creare layout diversi per dimensioni di telefono diverse. Ma se dovete scegliere tra una serie di dimensioni in un gruppo di device, testate sempre su quella più piccola: quelle più grandi funzioneranno di conseguenza!

Implementare i Breakpoint

Vediamo come tradurre i breakpoint in codice, con entrambi gli approcci:

Mobile-first (consigliato — si parte dal mobile e si aggiunge):

/* Mobile (0–411px): stili base */
.container { padding: 16px; }

/* Mobile large e superiori */
@media (min-width: 25.75rem) {
  .container { padding: 20px; }
}

/* Tablet portrait e superiori */
@media (min-width: 48rem) {
  .container { padding: 32px; }
}

/* Tablet landscape / laptop piccoli e superiori */
@media (min-width: 64rem) {
  .container { padding: 40px; }
}

/* Laptop grandi / desktop */
@media (min-width: 80rem) {
  .container { padding: 48px; max-width: 1200px; }
}

Desktop-first (si parte dal desktop e si riduce):

/* Laptop grandi / desktop: stili base */
.container { padding: 48px; max-width: 1200px; }

/* Tablet landscape / laptop piccoli e inferiori */
@media (max-width: 1279px) {
  .container { padding: 40px; max-width: none; }
}

/* Tablet portrait e inferiori */
@media (max-width: 1023px) {
  .container { padding: 32px; }
}

/* Mobile large e inferiori */
@media (max-width: 767px) {
  .container { padding: 20px; }
}

/* Mobile */
@media (max-width: 411px) {
  .container { padding: 16px; }
}

Usare valori custom è lecito

Per quanto perfetti siano i vostri breakpoint, non copriranno il 100% dei casi. È del tutto accettabile usare valori personalizzati quando un componente specifico lo richiede:

/* Breakpoint custom per questo componente (es. due colonne dalla fascia landscape) */
@media (min-width: 64rem) {
  .card-grid { grid-template-columns: 1fr 1fr; }
}

Ma se vi trovate a usare valori custom troppo spesso, è probabilmente un segnale che i vostri breakpoint di base sono nei punti sbagliati.

Provate voi!

Costruite un Layout Responsive

Aprite CodePen e create un layout che cambia a 3 breakpoint diversi. Ridimensionate il pannello Result per vedere le transizioni!

Risultato atteso

  • Mobile e mobile large (sotto 768px): una colonna, sfondo rosa
  • Da tablet portrait fino a laptop piccoli (768px–1279px): due colonne, sfondo azzurro
  • Laptop grandi / desktop (da 1280px): tre colonne, sfondo verde chiaro

Codice di partenza

<div class="page">
  <div class="card">Card 1</div>
  <div class="card">Card 2</div>
  <div class="card">Card 3</div>
</div>
.page {
  display: grid;
  gap: 16px;
  padding: 16px;
  background: #fecaca;
  min-height: 100vh; /* Vi spiegherò più avanti cosa vuol dire! */
}

.card {
  background: white;
  padding: 24px;
  border-radius: 8px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}

/* Aggiungete le media query!
   Suggerimento: usate min-width (mobile-first)
   e grid-template-columns per cambiare il numero di colonne */

Cosa esplorare

  • Aggiungete @media (min-width: 768px) (o 48rem) per passare a 2 colonne e cambiare sfondo
  • Aggiungete @media (min-width: 1280px) (o 80rem) per passare a 3 colonne e cambiare ancora sfondo
  • Provate a inserire una fascia intermedia a @media (min-width: 412px) (25.75rem) solo per rifinire padding o gap, senza cambiare il numero di colonne
  • Provate a convertire i breakpoint da px a rem (dividete per 16)
  • Cosa succede se aggiungete @media (prefers-color-scheme: dark) con colori scuri?
  • Provate a riscrivere tutto in desktop-first con max-width — ottenete lo stesso risultato?

Riepilogo

ConcettoSpiegazione
@mediaAt-rule che applica regole CSS in modo condizionale
Modello additivoLe regole si sommano, non si sostituiscono
Ordine nel CSSLe media query non cambiano la specificità — conta l'ordine
Mobile-first (min-width)Stili base per mobile, si aggiunge per schermi grandi
Desktop-first (max-width)Stili base per desktop, si riduce per schermi piccoli
rem nelle media queryRispettano la dimensione del font scelta dall'utente
hover / pointerQuery di interazione: tipo di input, non dimensione schermo
prefers-color-schemeRileva se l'utente preferisce il tema chiaro o scuro
prefers-reduced-motionRileva se l'utente vuole meno animazioni
printStili per la stampa su carta o PDF
BreakpointSoglie di larghezza che raggruppano i dispositivi
"Zone morte"I breakpoint vanno tra i cluster di dispositivi, non sui dispositivi
Valori customLeciti per casi specifici, ma non dovrebbero essere la norma

Le Custom Properties (Variabili CSS)

Le custom properties (proprietà personalizzate), comunemente chiamate "variabili CSS", sono una delle aggiunte più potenti al linguaggio. Permettono di definire un valore una volta e riutilizzarlo ovunque.

Sintassi:

/* Definizione: nome che inizia con -- */
.card {
  --colore-primario: #3498db;
  --spacing: 16px;
}

/* Utilizzo: la funzione var() legge il valore */
.card {
  background: var(--colore-primario);
  padding: var(--spacing);
}

Attenzione: non sono "globali" per magia. Le custom properties sono proprietà CSS a tutti gli effetti, come color o font-size. Questo significa che ereditano nel DOM dall'elemento genitore ai figli.

Se definite una variabile su .card, solo .card e i suoi figli possono usarla. Se un elemento fuori da .card prova a leggerla, non avrà effetto.

Il motivo per cui spesso le variabili sembrano "globali" è che vengono definite su :root (alias dell'elemento <html>), che è il genitore di tutto:

:root {
  --colore-primario: #3498db;
  --spacing: 16px;
}

/* Ora qualsiasi elemento nel documento può usarle */

Valori di fallback:

La funzione var() accetta un secondo argomento — un valore di riserva (fallback) che viene usato se la variabile non è definita:

.bottone {
  /* Se --altezza-minima non esiste, usa 32px */
  min-height: var(--altezza-minima, 32px);
}

Custom Properties e Design Responsive

Il vero potere delle custom properties per il responsive design è questo: cambiate il valore di una variabile dentro una media query, e tutti gli elementi che la usano si aggiornano automaticamente.

:root {
  --spacing: 8px;
}

@media (min-width: 25.75rem) {
  :root {
    --spacing: 12px;
  }
}

@media (min-width: 48rem) {
  :root {
    --spacing: 16px;
  }
}

@media (min-width: 64rem) {
  :root {
    --spacing: 24px;
  }
}

@media (min-width: 80rem) {
  :root {
    --spacing: 32px;
  }
}

Ora potete usare --spacing ovunque nel CSS, e il valore cambierà a ogni breakpoint:

.card {
  padding: var(--spacing);
  gap: var(--spacing);
  border-radius: var(--spacing);
}

Senza variabili dovreste ripetere le stesse dichiarazioni in ogni media query per ogni elemento. Con le variabili, la media query cambia un solo valore e tutto il layout si adatta.

Caso pratico: dimensione minima touch target

Come avete visto nella sezione sulle media query e sui breakpoint, sui dispositivi touch le aree cliccabili devono essere abbastanza grandi per un dito. Le linee guida Apple raccomandano un minimo di 44x44px.

Potete gestirlo con una sola variabile:

@media (pointer: coarse) {
  :root {
    --min-tap-height: 44px;
  }
}

.bottone {
  min-height: var(--min-tap-height, 32px);
}

.input-testo {
  min-height: var(--min-tap-height, 32px);
}

Con un puntatore preciso (mouse), --min-tap-height non è definita e si usa il fallback 32px. Con un puntatore grossolano (dito), la variabile vale 44px e tutti i componenti crescono — senza dover aggiungere media query a ogni componente.

Design Tokens

Usate le variabili come "token di design" (design tokens): valori centralizzati per spacing, colori e dimensioni che cambiano in un solo punto e si propagano ovunque. Meno ripetizioni, meno errori.

Frammenti di Variabili

Le variabili CSS possono contenere frammenti di valori — pezzi che da soli non sono un valore CSS completo, ma che si combinano come mattoncini.

Per capire questo concetto, serve conoscere un modo alternativo di definire i colori: HSL.

Mini-introduzione a HSL:

HSL sta per Hue, Saturation, Lightness (tonalità, saturazione, luminosità):

ComponenteCosa controllaValori
Hue (tonalità)Il colore sulla ruota cromatica0deg = rosso, 120deg = verde, 240deg = blu
Saturation (saturazione)Quanto il colore è vivace0% = grigio, 100% = colore pieno
Lightness (luminosità)Quanto è chiaro o scuro0% = nero, 50% = colore pieno, 100% = bianco
.esempio {
  color: hsl(210deg, 80%, 50%);  /* Un blu vivace */
}

Variabili come frammenti:

Ora possiamo usare una variabile per contenere solo la tonalità, e combinarla dentro hsl():

.card {
  --hue: 210deg;

  background: hsl(var(--hue), 80%, 50%);       /* Colore principale */
  border-color: hsl(var(--hue), 80%, 30%);      /* Versione scura */
  box-shadow: 0 2px 8px hsl(var(--hue), 60%, 70%); /* Versione chiara */
}

Cambiando un solo valore (--hue), l'intera palette del componente si aggiorna. Potete creare varianti di colore semplicemente ridefinendo la variabile:

.card--successo { --hue: 140deg; }  /* Verde */
.card--errore   { --hue: 0deg; }    /* Rosso */
.card--info     { --hue: 210deg; }  /* Blu */

Questo funziona perché le variabili CSS vengono valutate nel momento in cui vengono usate, non quando vengono definite. Potete anche combinare più variabili tra loro:

:root {
  --hue-primario: 210deg;
  --saturazione: 80%;
  --colore-primario: hsl(var(--hue-primario), var(--saturazione), 50%);
}

HSL e variabili per palette

HSL è molto più intuitivo di esadecimale o RGB quando dovete costruire palette di colori. Con HSL potete ragionare per "tonalità" e poi variare luminosità e saturazione. Con i frammenti di variabili CSS, questa tecnica diventa ancora più potente. In una prossima lezione vedremo altre funzioni ancora più potenti per definire e modificare i colori.

Provate voi!

Card con Variabili CSS

Aprite CodePen e create una card che cambia colori e spaziatura al variare della dimensione del viewport, usando le custom properties come "leva" nelle media query.

Risultato atteso

  • Su mobile: card con padding ridotto e tonalità calda (arancione/rosso)
  • Su schermi più grandi (da 768px / 48rem): card con padding ampio e tonalità fredda (blu)
  • Il cambio avviene modificando solo le variabili nella media query, non le proprietà della card

Codice di partenza

<div class="card">
  <h2 class="card__titolo">La mia card</h2>
  <p class="card__testo">
    Questa card usa custom properties per cambiare
    aspetto a breakpoint diversi.
  </p>
  <button class="card__bottone">Azione</button>
</div>
:root {
  --hue: 20deg;
  --spacing: 12px;
  --raggio: 4px;
}

/* Aggiungete una media query che cambi --hue, --spacing e --raggio */

.card {
  background: hsl(var(--hue), 70%, 95%);
  border: 2px solid hsl(var(--hue), 70%, 50%);
  border-radius: var(--raggio);
  padding: var(--spacing);
  max-width: 400px;
}

.card__titolo {
  color: hsl(var(--hue), 70%, 30%);
  margin: 0 0 var(--spacing) 0;
}

.card__testo {
  color: #333;
  margin: 0 0 var(--spacing) 0;
}

.card__bottone {
  background: hsl(var(--hue), 70%, 50%);
  color: white;
  border: none;
  padding: var(--spacing);
  border-radius: var(--raggio);
  cursor: pointer;
}

Cosa esplorare

  • Aggiungete @media (min-width: 768px) (48rem) e cambiate i valori di --hue, --spacing e --raggio su :root
  • Osservate quanti elementi si aggiornano cambiando solo 3 variabili
  • Provate un terzo breakpoint (es. 1024px / 64rem o 1280px / 80rem) con valori ancora diversi
  • Aggiungete una variabile --hue-accento per dare al bottone una tonalità diversa dal resto della card
  • Provate @media (prefers-color-scheme: dark) per creare una variante scura cambiando solo le variabili

La Funzione calc()

La funzione calc() permette di fare operazioni matematiche direttamente nel CSS — e soprattutto di mescolare unità diverse:

.sidebar {
  /* 100% della larghezza meno 64px per la sidebar */
  width: calc(100% - 64px);
}

Questo non si può ottenere in nessun altro modo: 100% e 64px sono unità diverse che il browser risolve in momenti diversi. calc() lascia che sia il browser a fare il calcolo al momento giusto.

Operatori disponibili:

OperatoreSignificato
+Addizione
-Sottrazione
*Moltiplicazione
/Divisione

Un vantaggio sottile: calc() rende il ragionamento leggibile. Confrontate:

/* Quale è più chiaro? */
.colonna { width: 14.285%; }
.colonna { width: calc(100% / 7); }  /* 1/7 dello spazio */

La seconda versione mostra l'intenzione — non solo il risultato.

Attenzione alla sintassi

Gli operatori + e - devono avere uno spazio su entrambi i lati. calc(100%-64px) non funziona, calc(100% - 64px) sì. Moltiplicazione e divisione non hanno questa restrizione, ma usare gli spazi è comunque consigliato per leggibilità.

calc() — Esempi Pratici

1. Conversione da pixel a rem

Come avete visto nella sezione sulle unità CSS, rem è preferibile a px per le dimensioni del testo. Ma ragionare in rem (multipli di 16) non è immediato. calc() risolve il problema:

h2 {
  /* Volete 18px ma in rem: */
  font-size: calc(18rem / 16);   /* = 1.125rem */
}

h3 {
  font-size: calc(24rem / 16);   /* = 1.5rem */
}

Il primo numero è il valore in pixel che avete in mente. Dividendo per 16, ottenete il valore in rem — e il CSS fa il calcolo per voi.

2. Combinare calc() con le variabili

calc() diventa ancora più potente insieme alle custom properties:

:root {
  --spacing: 8px;
}

@media (min-width: 48rem) {
  :root {
    --spacing: 16px;
  }
}

.card {
  padding: var(--spacing);
  border-radius: calc(var(--spacing) / 2);   /* Metà dello spacing */
  gap: calc(var(--spacing) * 1.5);            /* 1.5 volte lo spacing */
}

Con un solo valore di base (--spacing), derivate tutti gli altri in modo proporzionale. Quando --spacing cambia al breakpoint, tutti i valori derivati si ricalcolano automaticamente.

3. Spaziatura responsive proporzionale:

.griglia {
  --colonne: 3;
  --gap: 16px;

  display: grid;
  grid-template-columns: repeat(var(--colonne), 1fr);
  gap: var(--gap);
  /* Larghezza massima = spazio per le colonne + i gap */
  max-width: calc(300px * var(--colonne) + var(--gap) * (var(--colonne) - 1));
}

Le Unità Viewport

Le unità viewport misurano le dimensioni in base alla finestra del browser (viewport = la parte visibile della pagina):

UnitàSignificatoEquivalenza
vwViewport Width — larghezza del viewport1vw = 1% della larghezza
vhViewport Height — altezza del viewport1vh = 1% dell'altezza
vminLa dimensione minore tra larghezza e altezzaSu un telefono in verticale: 1vmin = 1vw
vmaxLa dimensione maggiore tra larghezza e altezzaSu un telefono in verticale: 1vmax = 1vh

Esempi:

/* Elemento largo quanto tutto il viewport */
.hero {
  width: 100vw;
  height: 50vh;   /* Alto metà del viewport */
}

/* Spaziatura tra lettere che cresce col viewport */
.titolo-grande {
  letter-spacing: 0.5vw;
}

Queste unità possono essere usate con qualsiasi proprietà che accetta valori di lunghezza: width, height, padding, margin, font-size, letter-spacing e molte altre.

vmin e vmax sono utili per elementi che devono adattarsi all'orientamento del dispositivo: su un telefono in verticale, vmin corrisponde alla larghezza (la dimensione più piccola); ruotando in orizzontale, vmin diventa l'altezza.

Attenzione alle trappole

Le unità viewport sono potenti, ma come vedrete nella prossima slide, hanno dei problemi reali su mobile e desktop. Usatele con consapevolezza.

Viewport Units — Attenzione alle Trappole

Le unità viewport hanno due problemi importanti che dovete conoscere:

Problema 1: 100vh su mobile non corrisponde all'area visibile

Quando caricate una pagina su un telefono, il browser mostra la barra degli indirizzi in alto e i pulsanti di navigazione in basso. Quando iniziate a scorrere, questa interfaccia scivola via, liberando più spazio.

Screenshot del browser mobile con UI espansa Screenshot del browser mobile con UI ridotta

Il valore 100vh si riferisce sempre all'altezza massima (con l'interfaccia del browser nascosta). Quando la pagina viene caricata e l'interfaccia è ancora visibile, un elemento 100vh sborda oltre lo schermo visibile.

Problema 2: 100vw su desktop include la scrollbar

L'unità vw misura la larghezza del viewport inclusa la scrollbar. Su mobile non è un problema (la scrollbar è trasparente e sovrapposta). Ma su desktop la scrollbar occupa spazio fisico (tipicamente 15–17px). Quindi width: 100vw causa un overflow orizzontale — la pagina diventa leggermente più larga del visibile.

Vedremo questo bug in dettaglio nella prossima slide.

Le alternative moderne: dvh, svh, lvh

I browser moderni supportano nuove unità viewport che risolvono il problema mobile:

UnitàSignificato
dvhDynamic — cambia quando la barra del browser appare/scompare
svhSmall — l'altezza minima (con tutta l'interfaccia browser visibile)
lvhLarge — l'altezza massima (interfaccia nascosta) — equivale a vh
.hero {
  /* Usa l'altezza effettiva visibile, che si aggiorna dinamicamente */
  min-height: 100dvh;
}

Attenzione

In generale: per l'altezza piena su mobile usate 100dvh (o 100svh se non volete che l'elemento si ridimensioni durante lo scroll). Per larghezze a tutto schermo, non usate 100vw — usate width: 100% sul body o sul contenitore. Vedrete il motivo nella prossima slide.

Il Bug di 100vw

Esiste un fenomeno molto comune legato al valore 100vw: una pagina web acquisisce una scrollbar orizzontale indesiderata che permette di scorrere di pochi pixel a destra.

Screenshot del browser desktop con scrollbar orizzontale indesiderata

La causa più frequente: 100vw

Ecco cosa succede quando usate width: 100vw su desktop: il viewport è largo, diciamo, 1200px. La scrollbar verticale occupa 15px. Lo spazio effettivo per il contenuto è 1185px. Ma 100vw vale 1200px — include la scrollbar. Risultato: 15px di overflow orizzontale.

La soluzione è semplice:

Per elementi a larghezza piena, usate width: 100% (che si riferisce al contenitore, non al viewport) invece di width: 100vw:

/* PROBLEMA: causa overflow orizzontale */
.full-width {
  width: 100vw;
}

/* SOLUZIONE: rispetta lo spazio disponibile */
.full-width {
  width: 100%;
}

Novità Chrome 145

Dalla versione 145 di Chrome (inizio 2026), 100vw sottrae automaticamente la larghezza della scrollbar in presenza di determinate condizioni.

Altre cause comuni di scrollbar orizzontale indesiderata:

  • Un'immagine o un video senza max-width: 100% che sborda dal contenitore
  • Una parola molto lunga che non va a capo (es. URL senza word-break: break-all)
  • Un elemento posizionato con valori negativi di left o right
  • Un elemento con margin negativo che esce dal flusso

Come trovarle: nel DevTools del browser, ispezionate gli elementi partendo dal <body> e scendendo nell'albero — l'elemento che supera la larghezza del body è il colpevole.

Regola pratica

Come regola pratica: evitate 100vw per le larghezze. L'unità vw è utile per calcoli proporzionali (es. 50vw, 0.5vw per letter-spacing), ma per "larghezza piena" il buon vecchio 100% funziona meglio e non ha il bug della scrollbar.

min(), max() e clamp()

Fino ad ora, per vincolare le dimensioni potevate usare min-width e max-width. Ma queste proprietà funzionano solo per larghezze e altezze — non esiste min-padding o max-font-size.

Le funzioni min(), max() e clamp() risolvono questo limite: sono valori, non proprietà, quindi si possono usare con qualsiasi proprietà che accetta valori numerici.

min() — prende il valore più piccolo:

.box {
  /* Il padding sarà 4vw OPPURE 32px, quello che è minore */
  padding: min(4vw, 32px);
}

Il padding cresce con il viewport, ma non supera mai 32px.

max() — prende il valore più grande:

.box {
  /* Il padding sarà almeno 16px, anche se 2vw è meno */
  padding: max(2vw, 16px);
}

Il padding cresce con il viewport, ma non scende mai sotto 16px.

clamp() — combina un minimo, un valore ideale e un massimo:

.colonna {
  /* Minimo 300px, ideale 65%, massimo 800px */
  width: clamp(300px, 65%, 800px);
}

clamp() prende tre argomenti: clamp(minimo, ideale, massimo). Il browser usa il valore ideale, ma lo vincola tra il minimo e il massimo.

clamp() equivale a combinare min-width, width e max-width:

/* Queste due regole fanno la stessa cosa: */
.colonna {
  width: clamp(300px, 65%, 800px);
}

.colonna {
  min-width: 300px;
  width: 65%;
  max-width: 800px;
}

Ma con clamp() è tutto in una riga, ed è utilizzabile per qualsiasi proprietà — non solo width e height.

Supporto browser

min(), max() e clamp() possono mescolare unità diverse (px, %, vw, rem) nello stesso calcolo, proprio come calc(). Sono supportate da tutti i browser moderni.

clamp() in Pratica

L'analogia del termostato (da Josh W. Comeau):

Pensate a clamp() come a un termostato. Impostate una temperatura ideale (es. 22 gradi), ma con un minimo (18 gradi) e un massimo (26 gradi). Il termostato cerca di mantenere la temperatura ideale, ma non va mai sotto il minimo né sopra il massimo.

clamp(18, 22, 26) — il valore "reale" può variare, ma resta sempre nell'intervallo.

1. Colonna di testo con larghezza fluida:

.articolo {
  /* Larga 65% del viewport, ma mai sotto 300px né sopra 800px */
  width: clamp(300px, 65%, 800px);
  /* Sicurezza extra: non sbordare mai dal viewport */
  max-width: 100%;
  margin-left: auto;
  margin-right: auto;
}

Il max-width: 100% è un'aggiunta importante: su schermi più stretti di 300px (alcuni telefoni piccoli), clamp() restituirebbe 300px e causerebbe overflow. max-width: 100% impedisce che l'elemento superi lo spazio disponibile.

2. Padding responsive senza media query:

.sezione {
  /* Padding che cresce col viewport, da 16px a 64px */
  padding: clamp(16px, 4vw, 64px);
}

Nessuna media query! Il padding si adatta fluidamente. Su un viewport da 400px: 4vw = 16px (clamp usa il minimo). Su un viewport da 1000px: 4vw = 40px (nel range). Su un viewport da 2000px: 4vw = 80px ma clamp lo limita a 64px.

3. Hero section con altezza vincolata:

.hero {
  /* Alta 80vh, ma non più di 500px e mai meno del contenuto */
  min-height: clamp(200px, 80vh, 500px);
}

Su schermi alti, l'hero non diventa eccessivamente grande. Su schermi bassi, mantiene almeno 200px di altezza.

Quando usare clamp()

clamp() è particolarmente utile per sostituire combinazioni di media query + min-width/max-width. Se vi trovate a scrivere tre media query solo per cambiare un padding, probabilmente un singolo clamp() fa lo stesso lavoro in modo più elegante.

Provate voi!

Contenitore Fluido con clamp()

Aprite CodePen e create un contenitore con larghezza e padding fluidi usando clamp(), senza media query. Ridimensionate il pannello Result per vedere il comportamento.

Risultato atteso

  • Il contenitore ha una larghezza che si adatta fluidamente al viewport
  • Il padding cresce e si riduce con il viewport, ma resta entro limiti definiti
  • Nessuna media query necessaria per ottenere l'effetto

Codice di partenza

<main class="container">
  <h1>Articolo di esempio</h1>
  <p>
    Questo contenitore usa clamp() per avere una larghezza
    fluida e un padding che si adatta al viewport.
    Ridimensionate la finestra per vedere l'effetto!
  </p>
  <p>
    Non servono media query: clamp() gestisce tutto da solo,
    con un minimo, un valore ideale e un massimo.
  </p>
</main>
body {
  margin: 0;
  font-family: system-ui, sans-serif;
  background: #f0f0f0;
}

.container {
  /* Provate clamp() per la larghezza:
     minimo 280px, ideale 90%, massimo 800px */
  width: 90%;
  max-width: 800px;
  margin: 32px auto;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);

  /* Provate clamp() per il padding:
     minimo 16px, ideale 5vw, massimo 48px */
  padding: 24px;
}

h1 {
  margin-top: 0;
}

Cosa esplorare

  • Sostituite width: 90%; max-width: 800px; con un solo width: clamp(280px, 90%, 800px) — aggiungete max-width: 100% come rete di sicurezza
  • Sostituite padding: 24px con padding: clamp(16px, 5vw, 48px)
  • Osservate come il layout si adatta fluidamente senza breakpoint
  • Provate ad applicare clamp() anche al font-size di h1: font-size: clamp(1.5rem, 4vw, 2.5rem)
  • Provate min(90%, 800px) come alternativa a clamp() per la larghezza — qual è la differenza?
  • Usate max(16px, 3vw) per un padding che non scende mai sotto 16px

Riepilogo

ConcettoSpiegazione
--nome: valoreDefinisce una custom property (variabile CSS)
var(--nome)Legge il valore di una custom property
var(--nome, fallback)Legge la variabile, con valore di riserva se non definita
EreditarietàLe custom properties ereditano nel DOM, non sono globali per magia
Variabili + media queryCambiate il valore di una variabile in una media query e tutto si aggiorna
Frammenti di variabiliLe variabili possono essere pezzi di valori (es. solo la tonalità in hsl())
hsl(h, s, l)Modello colore: tonalità, saturazione, luminosità
calc()Operazioni matematiche nel CSS, anche con unità miste
calc(Xrem / 16)Pattern per convertire da pixel (X) a rem
vw / vhPercentuali della larghezza / altezza del viewport
vmin / vmaxLa dimensione minore / maggiore del viewport
dvh / svh / lvhUnità viewport dinamica / piccola / grande (risolvono il problema mobile)
ScrollburglarsOverflow orizzontale accidentale — spesso causato da 100vw
100vw vs 100%100vw include la scrollbar, 100% no — preferite 100% per larghezze piene
min(a, b)Restituisce il valore più piccolo tra a e b
max(a, b)Restituisce il valore più grande tra a e b
clamp(min, ideale, max)Vincola il valore ideale tra un minimo e un massimo

I Ruoli del Testo nel Responsive

Una domanda sorprendentemente complessa: il testo deve diventare più grande o più piccolo su mobile rispetto a desktop?

La risposta dipende dal tipo di testo.

Testo del corpo (body text)

Il testo dei paragrafi e delle liste dovrebbe restare della stessa dimensione su tutti i dispositivi. Perché? Perché i produttori di dispositivi hanno già fatto il lavoro per voi: un testo a 16px su telefono e su desktop occupa circa lo stesso spazio nel campo visivo dell'utente, grazie al diverso rapporto tra dimensione dello schermo e distanza dagli occhi.

Il body text dovrebbe essere almeno 16px (1rem). Sotto questa soglia, l'utente deve avvicinare il telefono in modo scomodo o fare pinch-to-zoom continuamente.

Testo piccolo ma importante (didascalie, etichette)

Didascalie di foto, etichette di form, note a piè di pagina — questi testi sono spesso molto piccoli. Se il contenuto è importante, su mobile conviene aumentarne leggermente la dimensione per mantenere la leggibilità.

Titoli (heading)

I titoli grandi hanno il problema opposto: su uno schermo stretto, un heading a 2.5rem (40px) diventa ingombrante e occupa metà dello schermo. I titoli spesso vanno ridotti su mobile.

Testo piccolo importante  →  ↑ AUMENTA su mobile
Body text                 →  = RESTA UGUALE
Titoli grandi             →  ↓ DIMINUISCE su mobile

Ogni tipo di testo ha esigenze responsive diverse. Non esiste una regola unica.

Il body text a 16px

Il body text a 16px è lo standard del web: Facebook, Wikipedia, GitHub usano tutti dimensioni simili indipendentemente dallo schermo. Per siti con molto testo (blog, articoli), si arriva fino a 18-21px, ma la dimensione non cambia tra mobile e desktop.

La Trappola dello Zoom su iOS Safari

Ecco un problema reale che incontrerete sicuramente: i campi di input dei form (<input>, <select>) hanno di default un font-size piuttosto piccolo, che li rende difficili da leggere su mobile.

Per compensare, iOS Safari fa zoom automatico quando l'utente tocca un campo con testo più piccolo di 16px. L'intenzione è buona — rendere il testo leggibile — ma il risultato è fastidioso: la pagina si ingrandisce, si sposta, e l'utente deve fare pinch-to-zoom per tornare alla vista normale.

La soluzione è semplice: impostate il font-size degli input ad almeno 1rem (16px). Safari fa zoom solo sui campi sotto i 16px.

/* PROBLEMA: Safari zoomera' automaticamente */
input, select, textarea {
  font-size: 14px;
}

/* SOLUZIONE: niente zoom indesiderato */
input, select, textarea {
  font-size: 1rem;  /* 16px — la soglia magica */
}

Usate 1rem invece di 16px per rispettare anche le preferenze dell'utente (come avete visto nella sezione sulle unità CSS).

Attenzione

Questo è uno dei bug più comuni nei siti mobile. Se un utente vi segnala che "il sito zooma da solo quando tocco un campo", la causa è quasi certamente un input con font-size sotto i 16px. Controllatelo sempre!

Tipografia con Media Query — L'Approccio Manuale

Il primo modo per adattare i titoli ai diversi schermi è quello che già conoscete: le media query.

h1 {
  font-size: 1.5rem;  /* Mobile: 24px */
}

@media (min-width: 48rem) {
  h1 {
    font-size: 2rem;  /* Tablet portrait e superiori: 32px */
  }
}

@media (min-width: 80rem) {
  h1 {
    font-size: 2.5rem;  /* Laptop grandi / desktop: 40px */
  }
}

Questo approccio funziona, ed è perfettamente valido. Ma ha un limite: il testo salta da una dimensione all'altra in modo brusco. A 767px il titolo è 24px, a 768px diventa improvvisamente 32px. Non c'è una transizione graduale.

font-size
  40px ─────────────────────────────────── ●━━━━━━
                                           │
  32px ──────────────── ●━━━━━━━━━━━━━━━━━━●
                        │
  24px ━━━━━━━━━━━━━━━━━●
       ─────────────────┬──────────────────┬──────→ viewport
                      768px            1280px

Ogni volta che il viewport attraversa un breakpoint, il titolo cambia dimensione di colpo — come un interruttore che scatta.

E se il testo potesse scalare gradualmente, come un cursore che scorre senza scatti?

Responsive vs Fluid

L'approccio con media query è chiamato responsive (a gradini discreti). L'approccio graduale che vedremo ora è chiamato fluid (continuo). Non sono in competizione: sono due strumenti diversi per problemi diversi.

L'Idea della Tipografia Fluida

La tipografia fluida (fluid typography) è un approccio in cui il font-size scala in modo continuo con la larghezza del viewport, invece di saltare tra valori fissi ai breakpoint.

Il primo tentativo potrebbe essere usare le unità viewport che già conoscete dalla Sezione 3:

h1 {
  font-size: 5vw;  /* 5% della larghezza del viewport */
}

Su un viewport di 1000px, il titolo sarà 50px. Su 400px, sarà 20px. Il testo scala, ma ci sono due problemi:

Problema 1 — Nessun limite:

Su schermi molto grandi il testo diventa enorme, su schermi molto piccoli diventa illeggibile. A 320px il titolo sarebbe solo 16px — praticamente come il body text.

Screenshot showing fluid heading too small on a narrow viewport

Problema 2 — Accessibilità:

Le unità viewport ignorano le preferenze dell'utente. Se un utente aumenta il font-size predefinito nel browser, il testo in vw non cambia. Questo viola le linee guida WCAG, che richiedono che il testo sia scalabile almeno al 200%.

La soluzione? Combinare unità viewport con unità relative come rem, e usare clamp() per imporre dei limiti. Vediamo come costruire la formula passo per passo.

Attenzione

Non usate vw da solo per il font-size. Funziona come demo, ma in produzione viola i requisiti di accessibilità. La formula completa che vedremo nella prossima slide risolve entrambi i problemi.

La Formula — Passo per Passo

Costruiamo insieme la formula per un titolo che va da 1rem (16px) a 2rem (32px) tra un viewport di 768px (tablet portrait nel nostro set) e 1280px (laptop grandi / desktop).

Passo 1 — Definire gli estremi:

Viewport minimoViewport massimo
Larghezza viewport768px1280px
Font-size desiderato1rem (16px)2rem (32px)

Passo 2 — Calcolare il range di variazione:

Il font-size deve cambiare di: 32px - 16px = 16px
Il viewport cambia di: 1280px - 768px = 512px

Passo 3 — Calcolare il tasso di crescita (rate):

Per ogni pixel in più di viewport, il font cresce di:

rate = 16px / 512px = 0.03125

Passo 4 — Convertire in unità viewport:

Ricordate: 1vw = 1% della larghezza del viewport. Per convertire il tasso in vw:

rate in vw = 0.03125 × 100 = 3.125vw

Passo 5 — Comporre la formula:

Partiamo dalla dimensione minima (1rem) e aggiungiamo la parte variabile:

font-size: calc(1rem + 3.125vw);

Nota sulla formula

La formula esatta sarebbe: calc(16px + (100vw - 768px) × 16 / 512). Ma in pratica nessuno la scrive a mano: si usa clamp() con i limiti desiderati e un valore intermedio approssimato. L'importante è capire il principio: mescolare un valore fisso (rem) con un valore variabile (vw).

clamp() — Il Modo Pratico

Nella sezione sulle variabili CSS e le funzioni avete imparato clamp(minimo, ideale, massimo). Ora lo applichiamo alla tipografia fluida.

Invece di costruire formule complesse, usiamo clamp() per definire:

  • un minimo sotto cui il testo non scende mai
  • un valore ideale che scala con il viewport
  • un massimo oltre cui il testo non cresce mai
h1 {
  font-size: clamp(1.5rem, 1rem + 2.5vw, 3rem);
}

Scomponiamo:

  • 1.5rem (24px) — il titolo non sarà mai più piccolo di così, neanche su uno schermo minuscolo
  • 1rem + 2.5vw — il valore ideale: parte da 1rem (rispetta le preferenze utente) e aggiunge una porzione proporzionale al viewport
  • 3rem (48px) — il titolo non sarà mai più grande di così, neanche su un monitor ultrawide
font-size
  3rem ─────────────────────────────── ●━━━━━━ (massimo)
                                         ╱
                                       ╱   scala
                                     ╱     gradualmente
                                   ╱
  1.5rem ━━━━━━━━━━━━━━━━━━━━━━━━●              (minimo)
         ────────────────────────────────────→ viewport

Perché 1rem + 2.5vw e non solo 2.5vw?

Il 1rem nella parte centrale è fondamentale: garantisce che il font-size risponda alle preferenze dell'utente. Se l'utente raddoppia il font predefinito del browser, 1rem passa da 16px a 32px — e il titolo cresce di conseguenza. Con solo vw, le preferenze dell'utente verrebbero ignorate.

Regola pratica

Per la tipografia fluida: usate sempre clamp() con un valore ideale che mescola rem e vw. Il rem garantisce accessibilità, il vw garantisce fluidità. Questa tecnica è perfetta per heading e display text, ma non usatela per il body text — il testo del corpo sta già bene a 1rem su tutti i dispositivi.

Provate voi!

Create un Titolo con Tipografia Fluida

Aprite CodePen e create un heading con font-size fluido usando clamp(). Ridimensionate il pannello Result per vedere il testo scalare gradualmente!

Risultato atteso

  • Il titolo scala gradualmente quando ridimensionate il pannello
  • Il titolo non diventa mai più piccolo di 1.5rem né più grande di 3rem
  • Il paragrafo sotto resta sempre alla stessa dimensione
Screenshot showing fluid heading too small on a narrow viewport

Codice di partenza

<article class="contenuto">
  <h1 class="titolo-fluido">Tipografia Fluida</h1>
  <p class="corpo">
    Questo paragrafo usa un font-size fisso in rem.
    Non scala con il viewport — e va bene cosi'!
    Il body text deve restare leggibile e stabile.
  </p>
  <h2 class="sottotitolo-fluido">Anche i sottotitoli possono essere fluidi</h2>
  <p class="corpo">
    Ridimensionate il pannello e osservate la differenza
    tra i titoli (fluidi) e il testo del corpo (fisso).
  </p>
</article>
.contenuto {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
}

/* Titolo: tipografia fluida con clamp() */
.titolo-fluido {
  font-size: clamp(1.5rem, 1rem + 2.5vw, 3rem);
  line-height: 1.2;
  color: #1a1a2e;
}

/* Sottotitolo: anche questo fluido, ma con range diverso */
.sottotitolo-fluido {
  font-size: clamp(1.2rem, 0.8rem + 1.5vw, 2rem);
  line-height: 1.3;
  color: #16213e;
}

/* Body text: fisso in rem, come deve essere */
.corpo {
  font-size: 1rem;
  line-height: 1.6;
  color: #333;
}

Cosa esplorare

  • Ridimensionate il pannello da molto stretto a molto largo: i titoli scalano, il paragrafo no
  • Cambiate i valori dentro clamp(): provate clamp(1rem, 0.5rem + 4vw, 4rem) — che effetto ha?
  • Rimuovete il 1rem + dalla parte centrale (lasciate solo 2.5vw): il testo diventa troppo piccolo su schermi stretti
  • Provate ad applicare clamp() anche al body text: vedrete che non serve, il testo a 1rem è già perfetto
  • Confrontate con l'approccio a media query: sostituite clamp() con font-size fissi a breakpoint diversi — vedete la differenza tra "graduale" e "a scatti"?

Design Fluido Oltre la Tipografia

La stessa tecnica clamp() che avete usato per la tipografia funziona per qualsiasi proprietà CSS che accetta valori di lunghezza: padding, margini, gap, border-radius...

.card {
  /* Padding fluido: da 16px a 48px */
  padding: clamp(1rem, 0.5rem + 3vw, 3rem);

  /* Gap fluido tra elementi */
  gap: clamp(0.75rem, 0.5rem + 1.5vw, 2rem);

  /* Border-radius fluido */
  border-radius: clamp(8px, 1vw, 16px);
}

L'effetto è un design che respira: su schermi piccoli lo spazio si comprime per non sprecare pixel preziosi, su schermi grandi si espande per dare aria al contenuto.

Combinare tutto con le CSS variable:

Potete usare le variabili CSS (come nella sezione sulle variabili CSS e le funzioni) per creare un sistema di spaziatura fluido riutilizzabile:

:root {
  --spazio-sm: clamp(0.5rem, 0.3rem + 1vw, 1rem);
  --spazio-md: clamp(1rem, 0.5rem + 2vw, 2rem);
  --spazio-lg: clamp(1.5rem, 1rem + 3vw, 3rem);

  --testo-heading: clamp(1.5rem, 1rem + 2.5vw, 3rem);
  --testo-body: 1rem;  /* fisso — non serve fluido */
}

.hero {
  padding: var(--spazio-lg);
}

.hero h1 {
  font-size: var(--testo-heading);
  margin-bottom: var(--spazio-md);
}

.hero p {
  font-size: var(--testo-body);
}

Definite i valori fluidi una volta sola nelle variabili, e usateli ovunque. Se volete cambiare la scala, modificate un solo punto.

Responsive e Fluid insieme

Responsive (media query) e fluid (clamp) non sono in competizione. Il responsive è migliore quando il layout deve cambiare struttura (da una colonna a tre). Il fluid è migliore quando le dimensioni devono adattarsi gradualmente. Nella pratica, i siti migliori usano entrambi.

Provate voi!

Create una Card Fluida

Aprite CodePen e create una card dove sia il font-size sia il padding scalano con il viewport usando clamp(). Ridimensionate per vedere tutto adattarsi gradualmente!

Risultato atteso

  • La card ha padding che cresce su schermi larghi e si riduce su schermi stretti
  • Il titolo della card scala fluidamente
  • Tutto si adatta in modo armonioso, senza scatti

Codice di partenza

<div class="pagina">
  <div class="card">
    <h2 class="card-titolo">Design Fluido</h2>
    <p class="card-testo">
      Questa card usa clamp() per padding e font-size.
      Ridimensionate il pannello per vedere tutto scalare
      in modo graduale e armonioso.
    </p>
    <a href="#" class="card-link">Scopri di piu'</a>
  </div>

  <div class="card">
    <h2 class="card-titolo">Senza Breakpoint</h2>
    <p class="card-testo">
      Nessuna media query necessaria per l'adattamento
      delle dimensioni. Il layout si prende cura di se'.
    </p>
    <a href="#" class="card-link">Scopri di piu'</a>
  </div>
</div>
:root {
  --spazio-sm: clamp(0.5rem, 0.3rem + 1vw, 1rem);
  --spazio-md: clamp(1rem, 0.5rem + 2vw, 2rem);
  --spazio-lg: clamp(1.5rem, 1rem + 3vw, 3rem);
}

.pagina {
  display: grid;
  gap: var(--spazio-md);
  padding: var(--spazio-lg);
  min-height: 100vh;
  background: #f8f9fa;
}

.card {
  background: white;
  padding: var(--spazio-lg);
  border-radius: clamp(8px, 1vw, 16px);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.card-titolo {
  font-size: clamp(1.3rem, 1rem + 1.5vw, 2.2rem);
  color: #1a1a2e;
  margin-bottom: var(--spazio-sm);
}

.card-testo {
  font-size: 1rem;
  line-height: 1.6;
  color: #555;
  margin-bottom: var(--spazio-md);
}

.card-link {
  font-size: 1rem;
  color: #2563eb;
  text-decoration: none;
  font-weight: 600;
}

.card-link:hover {
  text-decoration: underline;
}

Cosa esplorare

  • Ridimensionate il pannello: padding, gap e titolo scalano tutti insieme, senza scatti
  • Cambiate i valori delle variabili --spazio-* in :root — tutto il layout si aggiorna
  • Provate a rendere fluido anche il font-size del body text (.card-testo): vedrete che non migliora, anzi peggiora la leggibilità su schermi stretti
  • Aggiungete grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)) a .pagina — le card si dispongono automaticamente in colonne senza media query
  • Confrontate: rimuovete tutti i clamp() e sostituiteli con valori fissi — il design perde la sua capacità di adattarsi

Riepilogo

ConcettoSpiegazione
Ruoli del testoBody text resta uguale; heading si riduce su mobile; testo piccolo importante si aumenta
Zoom iOS SafariInput con font-size sotto 16px causano zoom automatico. Fix: font-size: 1rem
Tipografia responsiveMedia query per cambiare font-size a breakpoint — funziona ma crea salti bruschi
Tipografia fluidaFont-size che scala gradualmente con il viewport, senza scatti
vw puroScala col viewport ma ignora le preferenze utente e non ha limiti — da evitare
rem + vwMescolare le due unità garantisce fluidità e rispetto delle preferenze utente
clamp() per il testoclamp(min, ideale, max) impone limiti al font-size fluido
Body textLasciatelo a 1rem — non serve renderlo fluido
Design fluidoLa stessa tecnica clamp() funziona per padding, margin, gap e altre proprietà
Variabili CSS fluideDefinite valori clamp() in variabili CSS per un sistema di spaziatura riutilizzabile

Il Limite delle Media Query

Finora abbiamo usato le media query per adattare il layout alla larghezza del viewport (la finestra del browser). Funziona bene per i layout di pagina, ma ha un limite importante.

Immaginate una scheda profilo che compare in più punti della stessa pagina — una griglia che può avere 3 colonne strette o 2 colonne più larghe a seconda del contenuto:

Griglia di schede profilo: layout verticale su colonne strette, layout orizzontale su colonne larghe

Quando le schede sono 3 per riga, lo spazio è stretto: la foto va sopra, le informazioni sotto. Quando le schede sono 2 per riga, lo spazio è maggiore: la foto può andare a sinistra, i dettagli a destra.

Il problema: le due schede sono nella stessa pagina, allo stesso identico viewport. Una media query misura la finestra del browser — la stessa per entrambe. Non sa quanto spazio ha la singola scheda nel suo specifico contesto.

Quello che vorremmo è che ogni scheda potesse reagire allo spazio del proprio contenitore, non alla larghezza della finestra.

Esiste uno strumento CSS che fa esattamente questo: i container query.

Container query

I container query (query sul contenitore) sono supportati da tutti i browser moderni dal 2023. Funzionano in modo simile alle media query, ma invece di misurare il viewport misurano la larghezza dell'elemento contenitore.

Container Query — La Sintassi

Per usare i container query servono due cose:

1. Dichiarare il contenitore

Con la proprietà container-type sull'elemento genitore:

.card-wrapper {
  container-type: inline-size;
}

2. Scrivere la query

Con @container sugli elementi figli:

@container (min-width: 25rem) {
  .card {
    display: flex;
    flex-direction: row;
  }
}

Esempio hello-world:

<style>
  section {
    container-type: inline-size;
    background-color: peachpuff;
    border: 2px solid;
  }

  @container (max-width: 12rem) {
    p {
      font-weight: bold;
      color: red;
    }
  }
</style>

<section>
  <p>
    Il testo diventa grassetto e rosso
    nei contenitori stretti.
  </p>
</section>

La struttura HTML per il caso della scheda:

<div class="card-wrapper">
  <article class="card">
    <img src="foto.jpg" alt="..." />
    <div class="card-body">
      <h3>Titolo</h3>
      <p>Descrizione...</p>
    </div>
  </article>
</div>

La proprietà container-type accetta tre valori:

ValoreCosa misuraQuando usarlo
inline-sizeSolo la larghezzaNella maggior parte dei casi — il valore raccomandato
sizeLarghezza e altezzaRaro — attenzione: rompe l'altezza automatica!
normalNulla (default)Non serve dichiararlo — nessun contenimento

Attenzione

Usate quasi sempre inline-size. Il valore size misura anche l'altezza, ma questo impedisce all'elemento di crescere automaticamente in base al contenuto — collassa a 0px di altezza se non impostate un'altezza esplicita. Nella pratica quotidiana non vi serve quasi mai.

La Regola d'Oro

Perché serve dichiarare esplicitamente un container? Perché i container query non possono semplicemente funzionare su qualsiasi elemento?

Il motivo è un problema fondamentale che ha bloccato questa funzionalità per quasi 20 anni. Per capirlo, considerate questa situazione.

width: fit-content è una proprietà CSS che fa sì che un elemento sia largo quanto il suo contenuto — si allarga e si restringe dinamicamente al cambiare del contenuto. Immaginate di usarla su un paragrafo e di volerci applicare una container query:

p {
  width: fit-content; /* la larghezza dipende dal contenuto */
}

@container (max-width: 10rem) {
  p strong {
    font-size: 3rem; /* questo rende il testo più grande... */
  }
}

Sembra ragionevole: se il paragrafo è stretto, ingrandiamo il testo. Ma pensateci bene — quando il font-size aumenta, le parole diventano più larghe. Questo allarga il paragrafo. Il paragrafo allargato allarga il contenitore oltre i 10rem. La condizione non è più vera. Il CSS viene rimosso. Il testo torna piccolo. Il contenitore si restringe. La condizione è di nuovo vera. Il CSS viene riapplicato…

→ Loop infinito. Il browser non raggiunge mai uno stato stabile.

Con le media query questo problema non esiste: nessuna regola CSS può cambiare la larghezza del viewport. Il viewport è immutabile.

La soluzione è il contenimento (containment): quando dichiarate container-type: inline-size, state dicendo al browser: "La larghezza di questo elemento non dipende dal suo contenuto." Questo spezza il ciclo — il contenitore diventa un riferimento fisso su cui i figli possono fare query.

La regola d'oro dei container query

Non cambiate ciò che state misurando. Con container-type: inline-size, la larghezza del contenitore è "blindata" — i figli non possono influenzarla. L'altezza invece resta libera di crescere normalmente. Ecco perché inline-size è la scelta pratica: vi permette di scrivere condizioni sulla larghezza (che è quella che vi interessa nel 99% dei casi), ma lascia l'altezza libera di adattarsi al contenuto.

Container Query in Pratica

Vediamo un esempio concreto: la stessa scheda profilo riutilizzata in colonne di larghezze diverse. Quando il contenitore è stretto la scheda si impila in verticale; quando è largo si apre in orizzontale. La scheda non sa nulla della pagina — risponde solo al proprio spazio.

/* 1. Il contenitore dichiara il contenimento */
.card-wrapper {
  container-type: inline-size;
}

/* 2. Layout base (stretto) — immagine sopra, testo sotto */
.card {
  display: flex;
  flex-direction: column;
}

.card img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

/* 3. Container query — layout largo, immagine a sinistra */
@container (min-width: 25rem) {
  .card {
    flex-direction: row;
  }

  .card img {
    width: 40%;
    aspect-ratio: 1 / 1;
  }
}

Potete mettere questa scheda ovunque nella pagina — in una sidebar stretta, in un contenuto principale largo, in una griglia con 2 o 3 colonne — e si adatterà automaticamente. Non cambiate una riga di CSS: è il contenitore che decide il layout.

Questo è il potere dei container query: il componente diventa autonomo. Non dovete scrivere CSS diverso per ogni posizione nella pagina.

Confronto con le media query:

Media QueryContainer Query
MisuraLa finestra del browserIl contenitore dell'elemento
Sintassi@media (min-width: X)@container (min-width: X)
PrerequisitoNessunocontainer-type sul genitore
Ideale perLayout di paginaComponenti riutilizzabili
Provate voi!

Card Adattiva con @container

Aprite CodePen e create una card che cambia da layout verticale a orizzontale usando i container query. Mettete la stessa card in contenitori di larghezze diverse per vederla adattarsi!

Risultato atteso

  • Nella colonna stretta (250px): la card è verticale — immagine sopra, testo sotto
  • Nella colonna larga (600px): la card è orizzontale — immagine a sinistra, testo a destra
  • La stessa card si adatta automaticamente al suo contenitore

Codice di partenza

<h2>Colonna stretta (250px)</h2>
<div class="wrapper-stretto">
  <article class="card">
    <img src="https://picsum.photos/400/300" alt="Foto esempio" />
    <div class="card-body">
      <h3>Titolo Card</h3>
      <p>Questa card cambia layout in base allo spazio disponibile.</p>
    </div>
  </article>
</div>

<h2>Colonna larga (600px)</h2>
<div class="wrapper-largo">
  <article class="card">
    <img src="https://picsum.photos/400/301" alt="Foto esempio" />
    <div class="card-body">
      <h3>Titolo Card</h3>
      <p>Questa card cambia layout in base allo spazio disponibile.</p>
    </div>
  </article>
</div>
.wrapper-stretto {
  width: 250px;
  /* Aggiungete container-type qui */
}

.wrapper-largo {
  width: 600px;
  /* Aggiungete container-type qui */
}

.card {
  display: flex;
  flex-direction: column;
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
  background: white;
}

.card img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

.card-body {
  padding: 1rem;
}

/* Aggiungete la container query per il layout orizzontale */
/* @container (min-width: 25rem) {
  ...
} */

Cosa esplorare

  • Aggiungete container-type: inline-size su entrambi i wrapper
  • Scrivete una @container (min-width: 25rem) che cambi flex-direction in row
  • Nella query, limitate la larghezza dell'immagine al 40% con width: 40%
  • Provate a cambiare la soglia: cosa succede con min-width: 15rem? E con min-width: 35rem?
  • Aggiungete un terzo wrapper con width: 400px — quale layout usa la card?

@supports — Miglioramento Progressivo

I container query sono una funzionalità relativamente recente. E se doveste supportare browser più vecchi che non li conoscono?

Il CSS offre un modo per verificare se il browser supporta una determinata proprietà: i feature query (query sulle funzionalità), scritti con @supports.

/* Questo blocco si attiva SOLO se il browser supporta container-type */
@supports (container-type: inline-size) {
  .card-wrapper {
    container-type: inline-size;
  }

  @container (min-width: 25rem) {
    .card {
      flex-direction: row;
    }
  }
}

La sintassi è simile a @media: si scrive una dichiarazione CSS tra parentesi, e se il browser la riconosce, applica gli stili dentro il blocco.

Perché non basta la doppia dichiarazione?

Per proprietà singole, potete semplicemente scrivere due valori — il browser usa l'ultimo che capisce:

.elemento {
  background: #3498db;              /* fallback */
  background: oklch(62% 0.2 250);   /* browser moderni */
}

Ma quando il comportamento moderno richiede più proprietà coordinate, la doppia dichiarazione non basta. Avete bisogno di raggruppare un intero set di stili che funzionano insieme:

/* Fallback: layout con Flexbox */
.griglia {
  display: flex;
  flex-wrap: wrap;
}

.griglia > * {
  flex: 1 1 300px;
}

/* Se il browser supporta Grid, usiamo Grid */
@supports (display: grid) {
  .griglia {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  }

  .griglia > * {
    flex: unset; /* Rimuoviamo le regole Flexbox non più necessarie */
  }
}

Progressive enhancement

Questo approccio si chiama progressive enhancement (miglioramento progressivo): partite da una base che funziona ovunque, poi aggiungete stili avanzati per i browser che li supportano. Il sito funziona sempre — su browser moderni funziona meglio.

@supports in Pratica

Vediamo quando e come usare @supports nella pratica quotidiana.

Sintassi completa:

/* Verifica positiva: il browser supporta la proprietà */
@supports (property: value) {
  /* stili moderni */
}

/* Verifica negativa: il browser NON supporta la proprietà */
@supports not (property: value) {
  /* stili di fallback */
}

/* Combinare più condizioni */
@supports (display: grid) and (container-type: inline-size) {
  /* solo se supporta ENTRAMBE le funzionalità */
}

Esempio pratico — card con container query e fallback:

/* BASE: funziona su tutti i browser */
.card {
  display: flex;
  flex-direction: column;
}

/* FALLBACK: usiamo una media query standard (tablet portrait del nostro set) */
@media (min-width: 48rem) {
  .card {
    flex-direction: row;
  }
}

/* UPGRADE: se il browser supporta i container query, usiamoli */
@supports (container-type: inline-size) {
  .card-wrapper {
    container-type: inline-size;
  }

  /* Rimuoviamo il comportamento della media query */
  @media (min-width: 48rem) {
    .card {
      flex-direction: column; /* Reset: lasciamo decidere al container */
    }
  }

  /* Il container query gestisce tutto */
  @container (min-width: 25rem) {
    .card {
      flex-direction: row;
    }
  }
}

Quando usare @supports:

SituazioneServe @supports?
Singola proprietà con fallback sempliceNo — basta la doppia dichiarazione
Intero blocco di stili che dipendono da una funzionalità — raggruppate con @supports
Container query con fallback a media query — esempio perfetto
Proprietà con supporto quasi universale (Flexbox, Grid base)No — supporto ormai al 99%+

Supporto browser

@supports ha supporto browser eccellente (99.5%+ dei browser). Potete usarlo con tranquillità. Ma non serve per ogni cosa: se la proprietà che state usando è già supportata ovunque, aggiungere @supports è solo rumore inutile nel codice.

Riepilogo

ConcettoSpiegazione
container-typeDichiara un elemento come contenitore misurabile dai figli
inline-sizeValore consigliato: misura solo la larghezza, lascia l'altezza libera
@containerAt-rule che applica stili in base alla dimensione del contenitore
Regola d'oroNon cambiate ciò che state misurando — il contenimento spezza i loop
Container vs MediaMedia query = viewport (layout di pagina). Container query = contenitore (componenti)
@supportsFeature query: applica stili solo se il browser supporta una proprietà
Progressive enhancementBase che funziona ovunque + upgrade per browser moderni
Doppia dichiarazioneSufficiente per singole proprietà, non per blocchi di stili coordinati

Grazie!

Il web è responsive by design — ora lo siete anche voi.

1 / 12
Indice slide · ⌘K

Indice slide

⌘K