16 settembre 2026

Tipografia fluida senza sorprese: clamp(), unità container e un trucco con @property

Quando il testo continua a crescere anche se il layout non può più farlo — e come rendere le dimensioni coerenti in tutta la UI.

La tipografia fluida con clamp() è comoda, ma può produrre risultati sgradevoli: font che continuano a crescere anche quando il contenitore ha già raggiunto la sua larghezza massima, oppure dimensioni incoerenti quando si introducono più container per layout responsivi. In questo articolo vediamo come spostare la logica dal viewport al contenitore con le unità cqi e come usare @property per “congelare” un valore calcolato, ottenendo coerenza tipografica anche in presenza di container annidati.

La tipografia fluida è uno di quei compromessi felici: meno breakpoint, più continuità visiva, un’interfaccia che “respira” con lo spazio disponibile. Il classico approccio con clamp() funziona bene… fino a quando smette di farlo.

In particolare, ci sono due problemi ricorrenti:

  1. Il font continua a crescere anche quando il layout non cresce più (wrapper con max-width).
  2. Passando alle unità basate su container, i font diventano incoerenti quando definisci più container per risolvere problemi di layout.

Vediamo come risolverli in modo pulito.


1) Il limite di clamp() basato sul viewport non “vede” il tuo wrapper

Un pattern tipico:

  • hai un wrapper centrato con max-width (es. 1100px);
  • dentro, card e titoli;
  • il font è fluido con qualcosa tipo:
h2 {
  font-size: clamp(1.25rem, 2vw + 1rem, 2rem);
}

Il problema: il viewport può continuare ad aumentare, ma il wrapper no. Se il testo continua a scalare “nel vuoto”, finisci con titoli troppo grandi rispetto allo spazio reale delle card.

Soluzione: usare unità container (cqi) invece di vw

Le container query units (come cqi) misurano la dimensione del contenitore, non del viewport. Perfetto: la tipografia cresce finché cresce lo spazio reale.

Attenzione però a un dettaglio fondamentale: le unità container funzionano davvero solo se hai dichiarato un container. In caso contrario, il browser ricade di fatto su un comportamento equivalente al viewport (ed è qui che molti pensano “non funziona”).

Esempio:

.wrapper {
  container-type: inline-size;
  /* opzionale: container-name: wrapper; */
}

h2 {
  font-size: clamp(1.25rem, 2cqi + 1rem, 2rem);
}

Risultato: quando la .wrapper raggiunge il suo max-width, anche la tipografia smette di crescere. Molto più naturale.

Nota di naming: chiamare la classe “container” oggi può creare ambiguità mentale, perché “container” è anche un concetto specifico delle container queries. Non è un divieto, ma conviene scegliere nomi più semantici (wrapper, layout, section, ecc.).


2) Il secondo problema: più container = font incoerenti

Quando sistemi il layout con container queries, è normale voler definire container più “locali”. Ad esempio:

  • .wrapper è container per il layout generale;
  • .card diventa container per decidere quando le card si impilano.
.wrapper { container-type: inline-size; }
.card    { container-type: inline-size; }

Ora però succede una cosa che manda in crisi chi mantiene design system: stessa regola CSS, font diversi su elementi apparentemente equivalenti.

Perché? Perché se un elemento calcola font-size usando cqi, quel cqi si riferisce al container più vicino. Se in un punto il container più vicino è la card, in un altro è il wrapper… i numeri cambiano.

Questa variabilità può essere desiderata in alcuni casi, ma spesso per la tipografia di UI è un problema: vuoi coerenza, non “font-size che dipende da dove sei capitato nell’albero”.


3) Il trucco: registrare una custom property con @property e “fissare” il valore

Qui entra in gioco una caratteristica poco sfruttata: le custom property registrate.

Una variabile CSS normale (--step-2) è una stringa; quando la ridefinisci, stai passando testo non tipizzato.

Con @property, invece, puoi dire al browser:

  • che tipo di valore conterrà (<length>);
  • qual è il valore iniziale;
  • se deve ereditare.

Questo permette al browser di calcolare e memorizzare un valore “computato”, non una stringa.

Registrazione

@property --step-2 {
  syntax: "<length>";
  initial-value: 1rem;
  inherits: true;
}

Definire lo step sul wrapper, basato sul wrapper

Mettiamo che tu voglia basare tutto su cqi del wrapper (quindi: coerente e limitato dal suo max-size).

.wrapper {
  container-type: inline-size;
  --step-2: clamp(1.25rem, 2cqi + 1rem, 2rem);
}

Fin qui nulla di speciale. La parte “strana” è la successiva: ridefinire la stessa variabile su alcuni discendenti, in modo da salvare il valore calcolato rispetto al wrapper.

Un approccio pratico è farlo sui figli diretti:

.wrapper > * {
  --step-2: var(--step-2);
}

Sembra inutile, ma con @property cambia tutto: questa riassegnazione porta il browser a materializzare il valore come length computata.

A quel punto, anche se dentro hai .card { container-type: inline-size; }, gli elementi che usano font-size: var(--step-2) continueranno a usare uno step coerente, derivato dal wrapper, non dal container più vicino.

Esempio d’uso:

.card h2 {
  font-size: var(--step-2);
}

Risultato: puoi usare container queries per il layout delle card (impilamento, overflow, ecc.) senza ritrovarti con titoli a grandezze diverse tra card “solo perché il container locale è diverso”.


4) E se in un punto volessi “resettare” e tornare al container locale?

A volte può servirti l’opposto: in certe componenti vuoi che lo scaling sia davvero locale.

Qui conviene tenere una variabile “reset” non registrata (stringa), e usarla per ripristinare la formula dove serve.

Esempio concettuale:

:root {
  --step-2-reset: clamp(1.25rem, 2cqi + 1rem, 2rem);
}

.wrapper {
  container-type: inline-size;
  --step-2: var(--step-2-reset);
}

.wrapper > * {
  --step-2: var(--step-2);
}

.card {
  container-type: inline-size;
  /* se vuoi che qui torni a essere locale */
  --step-2: var(--step-2-reset);
}

È un pattern da usare con criterio (perché riporta volutamente la variabilità), ma rende esplicito dove vuoi coerenza globale e dove vuoi scaling locale.


Sintesi: una tipografia fluida più “intelligente”

  • Se usi clamp() con unità viewport, il testo può crescere anche quando il wrapper è fermo: passa alle unità container (cqi).
  • Per farle funzionare, serve un container vero: container-type: inline-size.
  • Se definisci più container per risolvere il layout, rischi incoerenze tipografiche: registra la variabile con @property e riassegnala per fissare il valore computato.

L’implicazione pratica è interessante: puoi progettare una scala tipografica fluida “ancorata” al layout reale, mantenendo al tempo stesso la libertà di spezzare e ricomporre i componenti con container queries senza perdere coerenza visiva. È uno di quei piccoli accorgimenti che rendono un design system più prevedibile e un’interfaccia più stabile.