Guida interattiva a
Adattarsi a ogni schermo, ogni utente, ogni contesto
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:
E queste sono solo le principali!
Per aiutarci a costruire interfacce flessibili che affrontino queste sfide, il CSS offre strumenti molto potenti.
clamp(), min() e max()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.
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.
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 = 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.
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):
La conversione avviene "sotto il cofano". Nel CSS, lavorate solo con i pixel software — ed è tutto ciò che vi serve sapere.
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.
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?
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.
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 1xSenza 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.
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.
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!
Tutti i browser moderni includono una modalità responsive integrata nei DevTools:
Questo vi permette di simulare diverse larghezze di schermo senza uscire dal browser. Vedrete le media query attivarsi in tempo reale mentre ridimensionate.
La simulazione nei DevTools è ottima per lo sviluppo quotidiano, ma non cattura tutto. Per risultati affidabili:
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.
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!
github.com, wikipedia.org) e attivate la modalità responsive dei DevToolsQuando assegnate dimensioni nel CSS — font, margini, padding, larghezze — dovete specificare un'unità di misura. Le tre unità più importanti da conoscere sono:
| Unità | Nome | Riferimento |
|---|---|---|
px | Pixel | Dimensione fissa |
em | Em | Relativa al font-size dell'elemento (o del genitore) |
rem | Root em | Relativa 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.
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;
}
border: 1px solid)box-shadow)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.
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.
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.
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.
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à.
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:
| Valore | Calcolo | Risultato |
|---|---|---|
1rem | 1 × 16px | 16px |
1.5rem | 1.5 × 16px | 24px |
2rem | 2 × 16px | 32px |
0.875rem | 0.875 × 16px | 14px |
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! */
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.
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.
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!
px restano sempre della stessa dimensionerem crescono e si riducono con il font-size della radiceem si moltiplicano ad ogni livello di annidamento<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;
}
html { font-size } da 16px a 24px: quale titolo cresce e quale no?em: il testo diventa sempre più grande ad ogni livelloem.annidamento { font-size: 1.2em } in font-size: 1.2rem — la composizione scomparepx e in remQuando 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).
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.
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.
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.
L'ordine delle regole nel CSS conta sempre. Mettete le media query dopo le regole base che volete sovrascrivere, altrimenti non avranno effetto.
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.
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) */
}
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?
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.
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) {
/* ... */
}
Potete usare anche em al posto di rem: nelle media query, le due unità funzionano in modo pressoché identico.
Aprite CodePen e sperimentate con le media query. Ridimensionate il pannello Result per vedere le regole attivarsi!
<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;
}
} */
max-width: 767px in max-width: 411px — il punto di scatto si sposta sulla soglia "mobile" del setmax-width: 360px)min-width — ottenete lo stesso risultato?@media prima delle regole base. Cosa succede?Quando parliamo di "media query", pensiamo subito alla larghezza dello schermo. Ma le media query sanno fare molto di più!
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:
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: hover | hover: none | |
|---|---|---|
pointer: fine | Mouse, trackpad | Stilo |
pointer: coarse | — | Dito (touchscreen) |
hover: il dispositivo può muovere il cursore senza cliccare? (mouse sì, dito no)pointer: quanto è preciso il puntatore? fine = mouse/trackpad, coarse = ditoIl browser rileva automaticamente il dispositivo di input in uso, e queste query si aggiornano dinamicamente.
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.
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;
}
}
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à).
Le media query possono anche "agganciarsi" alle preferenze personali dell'utente, impostate nel sistema operativo o nel browser.
@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.
@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.
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.
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.
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.
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.
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:
| Range | Categoria | Breakpoint min-width |
|---|---|---|
| 0 – 411px | Mobile | — (stili base) |
| 412 – 767px | Mobile large | 412px / 25.75rem |
| 768 – 1023px | Tablet portrait | 768px / 48rem |
| 1024px – 1279px | Tablet landscape / Laptop piccoli | 1024px / 64rem |
| 1280px+ | Laptop grandi / Desktop | 1280px / 80rem |
Non esiste un set "perfetto" universale — dipende dal vostro design e dai dispositivi che volete supportare. Ma questo è un buon punto di partenza.
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!
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; }
}
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.
Aprite CodePen e create un layout che cambia a 3 breakpoint diversi. Ridimensionate il pannello Result per vedere le transizioni!
<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 */
@media (min-width: 768px) (o 48rem) per passare a 2 colonne e cambiare sfondo@media (min-width: 1280px) (o 80rem) per passare a 3 colonne e cambiare ancora sfondo@media (min-width: 412px) (25.75rem) solo per rifinire padding o gap, senza cambiare il numero di colonnepx a rem (dividete per 16)@media (prefers-color-scheme: dark) con colori scuri?max-width — ottenete lo stesso risultato?| Concetto | Spiegazione |
|---|---|
@media | At-rule che applica regole CSS in modo condizionale |
| Modello additivo | Le regole si sommano, non si sostituiscono |
| Ordine nel CSS | Le 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 query | Rispettano la dimensione del font scelta dall'utente |
hover / pointer | Query di interazione: tipo di input, non dimensione schermo |
prefers-color-scheme | Rileva se l'utente preferisce il tema chiaro o scuro |
prefers-reduced-motion | Rileva se l'utente vuole meno animazioni |
print | Stili per la stampa su carta o PDF |
| Breakpoint | Soglie di larghezza che raggruppano i dispositivi |
| "Zone morte" | I breakpoint vanno tra i cluster di dispositivi, non sui dispositivi |
| Valori custom | Leciti per casi specifici, ma non dovrebbero essere la norma |
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);
}
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.
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.
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à):
| Componente | Cosa controlla | Valori |
|---|---|---|
| Hue (tonalità) | Il colore sulla ruota cromatica | 0deg = rosso, 120deg = verde, 240deg = blu |
| Saturation (saturazione) | Quanto il colore è vivace | 0% = grigio, 100% = colore pieno |
| Lightness (luminosità) | Quanto è chiaro o scuro | 0% = 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 è 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.
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.
48rem): card con padding ampio e tonalità fredda (blu)<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;
}
@media (min-width: 768px) (48rem) e cambiate i valori di --hue, --spacing e --raggio su :root1024px / 64rem o 1280px / 80rem) con valori ancora diversi--hue-accento per dare al bottone una tonalità diversa dal resto della card@media (prefers-color-scheme: dark) per creare una variante scura cambiando solo le variabiliLa 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:
| Operatore | Significato |
|---|---|
+ | 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.
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à.
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 misurano le dimensioni in base alla finestra del browser (viewport = la parte visibile della pagina):
| Unità | Significato | Equivalenza |
|---|---|---|
vw | Viewport Width — larghezza del viewport | 1vw = 1% della larghezza |
vh | Viewport Height — altezza del viewport | 1vh = 1% dell'altezza |
vmin | La dimensione minore tra larghezza e altezza | Su un telefono in verticale: 1vmin = 1vw |
vmax | La dimensione maggiore tra larghezza e altezza | Su 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.
Le unità viewport sono potenti, ma come vedrete nella prossima slide, hanno dei problemi reali su mobile e desktop. Usatele con consapevolezza.
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.
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 |
|---|---|
dvh | Dynamic — cambia quando la barra del browser appare/scompare |
svh | Small — l'altezza minima (con tutta l'interfaccia browser visibile) |
lvh | Large — l'altezza massima (interfaccia nascosta) — equivale a vh |
.hero {
/* Usa l'altezza effettiva visibile, che si aggiorna dinamicamente */
min-height: 100dvh;
}
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.
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.
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%;
}
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:
max-width: 100% che sborda dal contenitoreword-break: break-all)left o rightmargin negativo che esce dal flussoCome 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.
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.
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.
min(), max() e clamp() possono mescolare unità diverse (px, %, vw, rem) nello stesso calcolo, proprio come calc(). Sono supportate da tutti i browser moderni.
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.
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.
Aprite CodePen e create un contenitore con larghezza e padding fluidi usando clamp(), senza media query. Ridimensionate il pannello Result per vedere il comportamento.
<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;
}
width: 90%; max-width: 800px; con un solo width: clamp(280px, 90%, 800px) — aggiungete max-width: 100% come rete di sicurezzapadding: 24px con padding: clamp(16px, 5vw, 48px)clamp() anche al font-size di h1: font-size: clamp(1.5rem, 4vw, 2.5rem)min(90%, 800px) come alternativa a clamp() per la larghezza — qual è la differenza?max(16px, 3vw) per un padding che non scende mai sotto 16px| Concetto | Spiegazione |
|---|---|
--nome: valore | Definisce 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 query | Cambiate il valore di una variabile in una media query e tutto si aggiorna |
| Frammenti di variabili | Le 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 / vh | Percentuali della larghezza / altezza del viewport |
vmin / vmax | La dimensione minore / maggiore del viewport |
dvh / svh / lvh | Unità viewport dinamica / piccola / grande (risolvono il problema mobile) |
| Scrollburglars | Overflow 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 |
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.
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.
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à.
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 è 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.
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).
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!
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?
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.
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:
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.
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.
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.
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).
| Viewport minimo | Viewport massimo | |
|---|---|---|
| Larghezza viewport | 768px | 1280px |
| Font-size desiderato | 1rem (16px) | 2rem (32px) |
Il font-size deve cambiare di: 32px - 16px = 16px
Il viewport cambia di: 1280px - 768px = 512px
Per ogni pixel in più di viewport, il font cresce di:
rate = 16px / 512px = 0.03125
Ricordate: 1vw = 1% della larghezza del viewport. Per convertire il tasso in vw:
rate in vw = 0.03125 × 100 = 3.125vw
Partiamo dalla dimensione minima (1rem) e aggiungiamo la parte variabile:
font-size: calc(1rem + 3.125vw);
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).
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:
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 minuscolo1rem + 2.5vw — il valore ideale: parte da 1rem (rispetta le preferenze utente) e aggiunge una porzione proporzionale al viewport3rem (48px) — il titolo non sarà mai più grande di così, neanche su un monitor ultrawidefont-size
3rem ─────────────────────────────── ●━━━━━━ (massimo)
╱
╱ scala
╱ gradualmente
╱
1.5rem ━━━━━━━━━━━━━━━━━━━━━━━━● (minimo)
────────────────────────────────────→ viewport
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.
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.
Aprite CodePen e create un heading con font-size fluido usando clamp(). Ridimensionate il pannello Result per vedere il testo scalare gradualmente!
<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;
}
clamp(): provate clamp(1rem, 0.5rem + 4vw, 4rem) — che effetto ha?1rem + dalla parte centrale (lasciate solo 2.5vw): il testo diventa troppo piccolo su schermi stretticlamp() anche al body text: vedrete che non serve, il testo a 1rem è già perfettoLa 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.
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 (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.
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!
<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;
}
--spazio-* in :root — tutto il layout si aggiornafont-size del body text (.card-testo): vedrete che non migliora, anzi peggiora la leggibilità su schermi strettigrid-template-columns: repeat(auto-fit, minmax(280px, 1fr)) a .pagina — le card si dispongono automaticamente in colonne senza media queryclamp() e sostituiteli con valori fissi — il design perde la sua capacità di adattarsi| Concetto | Spiegazione |
|---|---|
| Ruoli del testo | Body text resta uguale; heading si riduce su mobile; testo piccolo importante si aumenta |
| Zoom iOS Safari | Input con font-size sotto 16px causano zoom automatico. Fix: font-size: 1rem |
| Tipografia responsive | Media query per cambiare font-size a breakpoint — funziona ma crea salti bruschi |
| Tipografia fluida | Font-size che scala gradualmente con il viewport, senza scatti |
vw puro | Scala col viewport ma ignora le preferenze utente e non ha limiti — da evitare |
rem + vw | Mescolare le due unità garantisce fluidità e rispetto delle preferenze utente |
clamp() per il testo | clamp(min, ideale, max) impone limiti al font-size fluido |
| Body text | Lasciatelo a 1rem — non serve renderlo fluido |
| Design fluido | La stessa tecnica clamp() funziona per padding, margin, gap e altre proprietà |
| Variabili CSS fluide | Definite valori clamp() in variabili CSS per un sistema di spaziatura riutilizzabile |
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:
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.
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.
Per usare i container query servono due cose:
Con la proprietà container-type sull'elemento genitore:
.card-wrapper {
container-type: inline-size;
}
Con @container sugli elementi figli:
@container (min-width: 25rem) {
.card {
display: flex;
flex-direction: row;
}
}
<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:
| Valore | Cosa misura | Quando usarlo |
|---|---|---|
inline-size | Solo la larghezza | Nella maggior parte dei casi — il valore raccomandato |
size | Larghezza e altezza | Raro — attenzione: rompe l'altezza automatica! |
normal | Nulla (default) | Non serve dichiararlo — nessun contenimento |
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.
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.
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.
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.
| Media Query | Container Query | |
|---|---|---|
| Misura | La finestra del browser | Il contenitore dell'elemento |
| Sintassi | @media (min-width: X) | @container (min-width: X) |
| Prerequisito | Nessuno | container-type sul genitore |
| Ideale per | Layout di pagina | Componenti riutilizzabili |
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!
<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) {
...
} */
container-type: inline-size su entrambi i wrapper@container (min-width: 25rem) che cambi flex-direction in rowwidth: 40%min-width: 15rem? E con min-width: 35rem?width: 400px — quale layout usa la card?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.
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 */
}
}
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.
Vediamo quando e come usare @supports nella pratica quotidiana.
/* 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à */
}
/* 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;
}
}
}
| Situazione | Serve @supports? |
|---|---|
| Singola proprietà con fallback semplice | No — basta la doppia dichiarazione |
| Intero blocco di stili che dipendono da una funzionalità | Sì — raggruppate con @supports |
| Container query con fallback a media query | Sì — esempio perfetto |
| Proprietà con supporto quasi universale (Flexbox, Grid base) | No — supporto ormai al 99%+ |
@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.
| Concetto | Spiegazione |
|---|---|
container-type | Dichiara un elemento come contenitore misurabile dai figli |
inline-size | Valore consigliato: misura solo la larghezza, lascia l'altezza libera |
@container | At-rule che applica stili in base alla dimensione del contenitore |
| Regola d'oro | Non cambiate ciò che state misurando — il contenimento spezza i loop |
| Container vs Media | Media query = viewport (layout di pagina). Container query = contenitore (componenti) |
@supports | Feature query: applica stili solo se il browser supporta una proprietà |
| Progressive enhancement | Base che funziona ovunque + upgrade per browser moderni |
| Doppia dichiarazione | Sufficiente per singole proprietà, non per blocchi di stili coordinati |
Il web è responsive by design — ora lo siete anche voi.