font-size: 16px sur :root. Un media query à 768px pour passer à 14px. Un autre à 1200px pour revenir à 16px. Ou l'incontournable html { font-size: 62.5%; } pour que 1rem = 10px et "simplifier les calculs" du développeur au détriment de l'utilisateur.
Passons aux choses sérieuses.
En 2026, CSS permet de faire respirer la typographie sans media queries à rallonge, sans JavaScript, et sans PostCSS obligatoire, juste des fonctions natives bien utilisées. On construit, on explique.
Règle de base
rem partout, px nulle part !
Avant de rentrer dans le vif, une règle non négociable : toutes les valeurs de taille doivent être exprimées en rem, jamais en px. Fontes, marges, paddings, et surtout les valeurs dans clamp(), min(), max().
Le rem (root em) est relatif à la taille de fonte de :root. Si l'utilisateur a configuré 20px dans son navigateur, 1rem = 20px partout, le layout scale avec lui. En px, c'est figé : la préférence utilisateur est ignorée. C'est une non-conformité WCAG 1.4.4, et une mauvaise expérience pour tout le monde.
Les unités viewport (dvw, vh) ne se convertissent pas en rem, elles sont relatives au viewport, pas à la fonte. Elles s'utilisent dans calc() combinées à une base en rem, comme on va le voir.
Référence MDN, font-size et les unités relatives
Design 1 - 0 Rigidité
Pourquoi ça change tout pour le design ?
Pendant des années, la typographie web a été pensée par paliers : une taille pour mobile, une pour tablette, une pour desktop. Des valeurs fixes choisies un mardi matin en réunion de sprint, gravées dans le CSS pour les cinq prochaines années, des breakpoints arbitraires, et entre les deux, rien. Un titre calibré à 32px sur 768px est souvent trop petit à 900px et trop grand à 600px. Le designer compense à la marge, le développeur empile les media queries, et le résultat reste approximatif. Tout le monde est mécontent, personne ne le dit. Surtout pas le client qui a validé la maquette sur son écran 1440px et ne comprend pas pourquoi « ça fait bizarre » sur son téléphone.
La typographie fluide change le paradigme. Il n'y a plus de paliers, il y a une progression continue, définie par des bornes intentionnelles. Le designer fixe le minimum acceptable et le maximum souhaitable. Entre les deux, la fonte évolue proportionnellement à l'espace disponible. Le titre occupe toujours la bonne place dans la hiérarchie visuelle, quel que soit le viewport. Sans réunion. Sans ticket Jira. Sans « on verra à la recette ».
Le bénéfice final est une cohérence typographique sans friction : le designer définit l'intention, le navigateur exécute. Plus de compromis entre mobile et desktop, plus de valeurs arbitraires à maintenir, plus de régression à chaque nouveau format d'écran. Juste de la typographie qui fait son travail, discrètement, correctement, tout le temps.
clamp()
Trois valeurs, une formule.
clamp(minimum, valeur-idéale, maximum) retourne la valeur idéale, contrainte entre min et max. Quand la valeur idéale est inférieure au minimum, c'est le minimum qui s'applique, et inversement pour le maximum.
font-size: clamp(1rem, calc(0.97rem + 0.1dvw), 1.125rem);
/* soit : clamp(16px, 15.5px + 0.1dvw, 18px) */
- Minimum
1rem: jamais en dessous, même sur le plus petit écran - Valeur idéale
calc(0.97rem + 0.1dvw): progression linéaire selon la largeur du viewport - Maximum
1.125rem: jamais au-dessus, même sur écran 4K
La progression est douce et continue, pas de saut, pas de breakpoint. À 1000px de large, on est autour de 16px. À 1440px, vers 17px. À 2500px, on plafonne.
dvw plutôt que vw
Pourquoi ?
vw (viewport width) inclut les barres d'interface du navigateur dans son calcul sur mobile. Sur Safari iOS notamment, la barre d'adresse rétractable fausse la valeur, la fonte peut légèrement sauter quand la barre disparaît au scroll.
dvw (dynamic viewport width) se recalcule en tenant compte de l'interface visible réelle. Plus stable, plus précis. Support navigateurs correct depuis 2023.
dvw et dvh : les unités viewport dynamiques
vw et vh intègrent dans leur calcul les barres d'interface du navigateur, même rétractées. Sur Safari iOS notamment, la valeur de vh change subtilement au scroll quand la barre d'adresse disparaît, provoquant un saut visible sur les éléments dimensionnés avec cette unité.
Le CSS propose depuis 2023 quatre variantes pour chaque axe :
| Unité | Viewport pris en compte |
|---|---|
vw / vh |
Valeur historique, UI navigateur incluse |
svw / svh |
Small, UI complètement déployée (valeur minimale) |
lvw / lvh |
Large, UI complètement rétractée (valeur maximale) |
dvw / dvh |
Dynamic, recalcul en temps réel selon l'UI visible |
dvw est le bon choix pour la typographie fluide : la variation est imperceptible sur l'axe horizontal. dvh est plus délicat, sur mobile, le recalcul continu au scroll convient pour les fontes (variation minime) mais pas pour dimensionner des blocs entiers.
Pour les éléments fixés (modales, menus plein écran), svh est plus adapté : height: 100svh garantit que l'élément ne déborde jamais, barre d'adresse déployée ou non.
C'est pour cette raison que les titres utilisent vh (et non dvh) sur les interfaces tactiles, éviter le recalcul au scroll, et dvh uniquement sur pointer.
La base : respecter la préférence navigateur
Une valeur fixe en px sur :root écrase la préférence de fonte configurée dans le navigateur. L'utilisateur qui a réglé 20px se retrouve avec 16px, WCAG 1.4.4 non conforme.
:root {
font-size: max(100%, 13px);
}
100% sur :root est souvent mal compris. On pourrait croire qu'il hérite de la fonte système, mais :root n'a pas de parent. 100% se résout sur la valeur initiale du navigateur, généralement 16px. Ce que max(100%, 13px) fait réellement : max(16px, 13px) = 16px dans le cas standard.
Le plancher à 13px ne joue que si l'utilisateur a configuré une fonte inférieure à 13px, cas marginal. L'intérêt de cette écriture est l'intention explicite : on part de la valeur navigateur, on pose un minimum lisible, on n'impose rien.
La fonte fluide
Sur cette base, on définit une variable --font-size fluide, appliquée sur body :
:root {
font-size: max(100%, 13px);
&:not(.contrast) {
--font-size: clamp(1rem, calc(0.9688rem + 0.1dvw), 1.125rem);
}
&.contrast {
--font-size: clamp(1.0625rem, calc(1.0313rem + 0.1dvw), 1.1875rem);
}
}
La variante .contrast cible les utilisateurs avec des besoins visuels accrus. Leur servir une fonte plus petite qu'en mode normal serait contre-productif, c'est exactement ce qui arrive avec une valeur fixe arbitraire. Ici, même formule, même progression, plancher et plafond décalés d'1px vers le haut. La fluidité est préservée, la hiérarchie aussi.
Gérer les titres
Un clamp et aucun breakpoint.
Un h1 fixe à 24px sur écran 2560px est microscopique.
À 32px sur mobile 320px, illisible en deux mots par ligne.
La solution : un clamp calibré sur les deux axes viewport, sans breakpoint.
h1 {
/*
Titres longs, fluidité dvw+vh
- Plancher : 28px petits mobiles (vh contraint), 32px sinon
- Fluide : moyenne dvw+vh, activation ~768px
- Plafond : 48px max, bridé par dvw sur ultra-wide
*/
font-size: clamp(
max(1.75rem, min(2rem, 4vh)),
calc((3.6dvw + 3.6vh) / 2),
min(3rem, max(4dvw, 3.5vh))
);
}
- Très petit mobile :
4vh < 28pxsur 320×568, le plancher descend à 28px. Le titre reste lisible sans jamais écraser la ligne. - Mobile courant : plancher à 32px jusqu'à ~768px. La valeur fluide n'est pas encore active, la taille est fixe et prévisible.
- Tablette et au-delà : la moyenne
(3.6dvw + 3.6vh) / 2prend le relais. Neutre en rotation : portrait ou paysage, la progression reste stable. - Grand écran :
min(3rem, max(4dvw, 3.5vh))bride proprement. Sur ultra-wide,4dvwcontient la croissance ; sur écran haut,3.5vhprend le relais.
Pas de breakpoint, la continuité est assurée par les bornes dynamiques elles-mêmes. Il n'y a aucun saut perceptible entre mobile et desktop.
Estimations par viewport
| Viewport | Orientation | Plancher | Fluide | Plafond | Résultat |
|---|---|---|---|---|---|
| 320×568 | Portrait | 28px | 15.8px | — | 28px |
| 375×812 | Portrait | 32px | 21.4px | — | 32px |
| 480×854 | Portrait | 32px | 24.4px | — | 32px |
| 768×1024 | Portrait | 32px | 32.4px | — | 32.4px |
| 1024×768 | Paysage | 28px | 32.4px | — | 32.4px |
| 1368×768 | Paysage | — | 39.3px | 38.9px | 38.9px |
| 1440×900 | Paysage | — | 41.9px | 43.2px | 41.9px |
| 1920×1080 | Paysage | — | 51.8px | 44.8px | 44.8px |
Support
clamp() est disponible dans tous les navigateurs modernes depuis juillet 2020.
| Navigateur | clamp() | dvw / vh |
|---|---|---|
| Chrome/Edge | 79+ | 108+ |
| Firefox | 75+ | 116+ |
| Safari desktop | 13.1+ | 16.4+ |
| Safari iOS | 13.4+ | 15.8+ |
| Samsung Internet | 12+ | 21+ |
| Internet Explorer | Mouarf | hein quoi ? |
Le résultat
:root {
/* Assure une font-size mini à 13px tout en respectant la configuration utilisateur */
font-size: max(100%, 13px);
&:not(.contrast) {
--font-size: clamp(1rem, calc(0.9688rem + 0.1dvw), 1.125rem);
}
&.contrast {
--font-size: clamp(1.0625rem, calc(1.0313rem + 0.1dvw), 1.1875rem);
}
h1 {
@media (any-hover: hover) {
font-size: clamp(
max(1.75rem, min(2rem, 4dvh)),
calc((3.6dvw + 3.6dvh) / 2),
min(3rem, max(4dvw, 3.5dvh))
);
}
/* pas de dvh sur tactile */
@media (any-hover: none) {
font-size: clamp(
max(1.75rem, min(2rem, 4vh)),
calc((3.6dvw + 3.6vh) / 2),
min(3rem, max(4dvw, 3.5vh))
);
}
}
/* à étendre si besoin… */
}
Pas de media query pour la fonte de base.
Pas de valeur fixe arbitraire.
La préférence utilisateur est respectée, le mode contrasté est cohérent, les titres s'adaptent aux deux axes du viewport.
Et zéro ligne de JavaScript…
That's all, Folks !