Les unités CSS enfin claires : px, em, rem, %, vw/vh

Une largeur en %, une police en rem, une hauteur en vh : on recopie ces unités dans ses premières feuilles de style sans trop savoir ce qu'elles mesurent, et ça tient. Jusqu'au jour où un texte imbriqué devient énorme sans raison, où un padding-top: 10% ne fait pas du tout 10 % de la hauteur attendue, ou où une bande en 100vw fait apparaître une barre de défilement horizontale. Ces trois surprises ont la même origine : chaque unité relative se réfère à quelque chose de précis, et ce quelque chose n'est pas toujours celui qu'on imagine.

Méthode habituelle de ce blog : chaque valeur citée ici a été mesurée pendant la rédaction, le 21 septembre 2026 (et le 2 novembre 2026 pour les compléments sur vw et vh), dans Chromium 153.0.8010.12 (le navigateur de Playwright 1.63.0, même moteur que Chrome) piloté depuis Node 20.19.4 sur Windows 11. Les tailles sont lues avec getComputedStyle, c'est-à-dire la valeur que le navigateur a réellement calculée, pas celle que j'ai écrite. Si le vocabulaire des boîtes et des marges est encore flou, le modèle de boîte est le préalable utile à tout ce qui suit.

La seule question qui compte : relatif à quoi

Il n'y a pas d'unités faciles et d'unités compliquées. Il y a une unité absolue, px, et des unités relatives qui ont chacune un point d'ancrage différent. Retenir le point d'ancrage suffit à prévoir le résultat, et c'est tout ce que les sections suivantes détaillent, mesures à l'appui.

L'unité... ... se calcule à partir de px rien : c'est la référence em la taille de police de l'élément rem celle de la balise html % le bloc conteneur, et ça dépend de la propriété vw / vh la fenêtre d'affichage Le point d'ancrage suffit à prévoir la valeur calculée.
Chaque unité relative a un point d'ancrage unique : c'est lui qu'il faut retenir, pas une recette.

px : fixe dans la page, pas sur l'écran

Le pixel CSS est l'unité de référence : font-size: 16px donne 16 pixels, quel que soit le contexte. C'est sa force, et c'est aussi sa limite, comme la section sur rem le montrera.

Un point mérite d'être levé tout de suite, parce qu'il trompe beaucoup de débutants : un pixel CSS n'est pas un point de votre dalle. La spécification le définit comme un 96e de pouce, soit px = 1/96th of 1in dans CSS Values and Units Module Level 4, formulation reprise par MDN sous la forme 1px = 1in / 96 (page sur le type <length>, consultée le 21 septembre 2026). Sur un écran à haute densité, le navigateur dessine donc un pixel CSS avec plusieurs points physiques. Je l'ai vérifié en capturant un carré de 100 pixels CSS de côté, d'abord sur un écran simulé à densité 1, puis à densité 2 : le fichier PNG obtenu fait 100 par 100 points dans le premier cas, 200 par 200 dans le second. Le carré n'a pas changé de taille apparente, le navigateur a simplement eu deux fois plus de points pour le dessiner.

/* 100 pixels CSS, quelle que soit la densité de l'écran */
#carre {
  width: 100px;
  height: 100px;
}

em : la taille du texte de l'élément, avec effet boule de neige

L'unité em vaut la taille de police calculée de l'élément. Sur une propriété autre que font-size, la règle est directe : dans une boîte réglée sur font-size: 20px, un padding: 1em donne un remplissage mesuré à 20 pixels sur les quatre côtés. Le rembourrage suit la taille du texte, ce qui est exactement ce qu'on veut sur un bouton dont l'air autour du libellé doit grandir avec le libellé.

#boite {
  font-size: 20px;
  padding: 1em; /* mesuré : 20px en haut comme à gauche */
}

Sur la propriété font-size elle-même, la référence change : em vaut alors la taille de police héritée, donc celle du parent. C'est là que l'effet boule de neige apparaît. Avec une racine à 16 pixels et une règle font-size: 1.5em appliquée à des blocs imbriqués, j'ai mesuré 24 pixels au premier niveau, 36 au deuxième, 54 au troisième. Chaque niveau multiplie le précédent, et personne n'a écrit 54 nulle part.

html { font-size: 16px; }
.em div { font-size: 1.5em; }   /* 24px, puis 36px, puis 54px en imbriqué */
.rem div { font-size: 1.5rem; } /* 24px aux trois niveaux */
racine html : 16 px font-size: 1.5em niveau 1 : 24 px niveau 2 : 36 px niveau 3 : 54 px font-size: 1.5rem niveau 1 : 24 px niveau 2 : 24 px niveau 3 : 24 px Valeurs lues avec getComputedStyle dans Chromium 153.
Le même 1.5 donne 54 px au troisième niveau en em, et 24 px en rem : l'un se cumule, l'autre non.

rem : la racine, donc le réglage du lecteur

L'unité rem se réfère toujours à la taille de police de l'élément racine, la balise html. Dans le même test imbriqué, 1.5rem donne 24 pixels aux trois niveaux : l'imbrication n'a plus aucun effet. C'est ce qui en fait l'unité par défaut raisonnable pour les tailles de texte et les espacements d'un site.

Il y a une deuxième raison, moins connue et bien plus importante. La taille de police de la racine n'est pas une constante du web : c'est un réglage du navigateur, que le lecteur peut changer dans ses préférences (dans Chrome, Paramètres puis Apparence, Taille de police). Pour le vérifier autrement qu'en croyant sur parole, j'ai lancé Chromium sur un profil utilisateur neuf dont la préférence de taille standard était posée à 24 pixels, puis j'ai mesuré deux paragraphes, l'un en font-size: 16px, l'autre en font-size: 1rem.

Profil par défaut   racine 16px | paragraphe en px 16px | paragraphe en rem 16px
Réglage à 24 px     racine 24px | paragraphe en px 16px | paragraphe en rem 24px

Le résultat est net : le texte en rem grandit avec le réglage, le texte en px reste figé à 16 pixels. Écrire toutes ses tailles de texte en pixels revient donc à ignorer une préférence que le lecteur a prise pour une raison, souvent une raison de confort visuel. Ce réglage a beau être discret, il ne coûte rien de le respecter : c'est le même chiffre divisé par 16, et mettre en forme le texte en CSS devient même plus simple à raisonner une fois que 1 vaut une taille de base.

Le pourcentage : sa référence change avec la propriété

Le pourcentage se calcule sur le bloc conteneur, mais pas toujours sur la même dimension de ce bloc. Sur width, l'intuition marche : dans un conteneur de 400 pixels de large, width: 50% est mesuré à 200 pixels. Sur les marges et le rembourrage, l'intuition tombe. J'ai placé un enfant dans un conteneur de 400 pixels de large sur 200 de haut, avec padding-top: 10% et margin-top: 10% : les deux valeurs calculées sont de 40 pixels, soit 10 % de la largeur, et non 20 pixels qui seraient 10 % de la hauteur.

#conteneur { width: 400px; height: 200px; }
#enfant {
  padding-top: 10%;  /* mesuré : 40px, soit 10% de la LARGEUR */
  margin-top: 10%;   /* mesuré : 40px également */
  width: 50%;        /* mesuré : 200px */
}

Ce n'est pas une bizarrerie de Chromium mais la règle écrite : la spécification CSS Box Model Level 3, recommandation du W3C du 11 avril 2024, indique pour padding-top comme pour margin-top que les pourcentages se réfèrent à la largeur logique du bloc conteneur. Loin d'être un piège gratuit, c'est ce qui permet la vieille astuce du bloc à proportions fixes, où un padding-top en pourcentage réserve une hauteur proportionnelle à la largeur. Les autres façons de dimensionner une boîte sont détaillées dans dimensions et positionnement en CSS.

Dernier cas : sur font-size, le pourcentage se comporte exactement comme em. Dans un parent à 20 pixels, font-size: 150% et font-size: 1.5em donnent tous deux 30 pixels mesurés. Deux écritures, un seul résultat.

line-height : le cas où le nombre bat le pourcentage

Cette propriété mérite sa section parce qu'elle accepte quelque chose qu'aucune autre propriété de dimension n'accepte : un nombre sans unité. Et la différence avec le pourcentage ne se voit que chez les descendants. J'ai comparé deux parents identiques réglés sur font-size: 20px, l'un avec line-height: 1.5, l'autre avec line-height: 150%, chacun contenant un enfant à font-size: 40px.

line-height: 1.5    parent 30px | enfant à 40px : 60px
line-height: 150%   parent 30px | enfant à 40px : 30px

Les deux parents sont à 30 pixels, mais l'enfant du second hérite de la valeur déjà calculée, 30 pixels, pour un texte de 40 pixels de haut : les lignes se chevauchent. Avec le nombre, c'est le facteur qui est hérité, et chaque descendant recalcule son interligne sur sa propre taille. D'où la règle simple : pour line-height, le nombre sans unité, toujours.

body { line-height: 1.5; } /* hérité comme facteur, jamais comme valeur figée */

vw en CSS : 1 % de la largeur de la fenêtre

1vw vaut un centième de la largeur de la fenêtre d'affichage (le viewport), quel que soit l'élément sur lequel on l'écrit. Contrairement au pourcentage, vw ne regarde jamais le parent : un bloc en 50vw fait la moitié de l'écran, même enfermé dans une colonne de 300 pixels. J'ai refait la mesure le 2 novembre 2026, dans le même Chromium 153.0.8010.12 : 50vw vaut 500 pixels dans une fenêtre de 1000, 720 dans une fenêtre de 1440 et 195 sur un téléphone émulé de 390 pixels de large. Petit détail qui surprend dans l'inspecteur : 1vw s'y affiche à 14,3906 pixels sur la fenêtre de 1440 et non à 14,4, et à 3,89062 sur le téléphone au lieu de 3,9, deux valeurs qui tombent sur des multiples exacts d'un 64e de pixel, la grille sur laquelle Chromium range ses longueurs.

Le piège classique de vw est la barre de défilement. Quand la page défile verticalement, la barre prend de la place, et 100vw l'ignore. Dans un Chromium à fenêtre visible, avec un contenu assez haut pour faire défiler, j'ai relevé le 21 septembre window.innerWidth à 1000 mais document.documentElement.clientWidth à 985 : l'élément large de 100vw fait donc 1000 pixels dans un espace de 985, et le navigateur ajoute une barre de défilement horizontale pour 15 pixels de trop. Sur le téléphone émulé, où la barre se superpose au contenu, les deux largeurs valent 390 et le bandeau ne déborde pas : le bug ne se voit qu'au bureau, ce qui explique qu'il passe souvent inaperçu.

/* Le classique bandeau pleine largeur qui fait défiler la page horizontalement */
#bandeau { width: 100vw; }
/* Version sans surprise : la largeur du bloc conteneur, barre déduite */
#bandeau { width: 100%; }

L'autre usage tentant est la taille de texte en vw, pour un titre qui grandit avec l'écran. Seul, c'est une mauvaise idée : font-size: 5vw donne 72 pixels sur la fenêtre de 1440 mais 19,5 sur le téléphone, et surtout il ignore le réglage de taille de police du lecteur vu plus haut. J'ai posé ce réglage à 24 pixels au lieu de 16 (par le protocole de débogage du navigateur, Page.setFontSizes) sur une fenêtre de 1000 pixels : un titre en 2.8vw reste à 28 pixels, alors que le même titre écrit avec clamp(1rem, 0.5rem + 2vw, 2rem) passe de 28 à 32 pixels. La part en rem suit le lecteur, la part en vw suit l'écran, et les deux bornes empêchent le titre de devenir minuscule sur mobile (16 pixels mesurés sur le téléphone) ou démesuré sur un grand écran. Le reste des réglages de texte est détaillé dans changer la police et la taille du texte en CSS.

/* Titre fluide : jamais moins de 1rem, jamais plus de 2rem, et sensible au réglage du lecteur */
h1 { font-size: clamp(1rem, 0.5rem + 2vw, 2rem); }

vh en CSS : la hauteur de la fenêtre, et le piège de 100vh sur mobile

1vh vaut un centième de la hauteur de la fenêtre d'affichage : 100vh est mesuré à 600 pixels dans une fenêtre de 1000 par 600, à 900 dans une fenêtre de 1440 par 900. C'est l'unité des sections « plein écran », avec une précaution que le banc rend évidente. Un bloc en height: 100vh qui contient 800 pixels de contenu reste à 600 pixels de haut : le contenu déborde de 200 pixels et le pied de page, placé juste après, se retrouve dessous. Avec min-height: 100vh, le même bloc mesure 800 pixels, et redescend à 600 quand il ne contient que 100 pixels de contenu.

/* Au moins la hauteur de l'écran, mais libre de grandir avec son contenu */
.plein-ecran { min-height: 100vh; }

Sur mobile, c'est vh lui-même qui pose problème, parce que la hauteur utile change quand la barre d'adresse se rétracte au défilement. Le standard a répondu avec trois familles d'unités : svh pour la petite fenêtre, interfaces déployées, lvh pour la grande, interfaces rétractées, et dvh qui suit dynamiquement la hauteur du moment (CSS Values and Units Module Level 4, section des unités relatives à la fenêtre, consultée le 21 septembre 2026). La spécification précise que vh correspond à la grande fenêtre, ce qui explique le symptôme bien connu d'un bloc en 100vh dont le bas passe sous la barre d'adresse tant qu'elle est visible. Mesure honnête de ma part : ni mon banc de bureau ni l'émulation mobile de Chromium ne dessinent de barre d'adresse rétractable, et les quatre unités y donnent la même valeur, 600 pixels au bureau, 844 sur le téléphone émulé. La différence ne se constate que sur un vrai navigateur mobile.

Côté disponibilité, ces trois familles ne sont plus une nouveauté : Chrome 108, Firefox 101 et Safari 15.4 les prennent en charge, d'après les données de compatibilité de MDN consultées le 21 septembre 2026. Pour un plein écran mobile, min-height: 100dvh est aujourd'hui le choix par défaut, et la logique des points de rupture qui va avec est traitée dans responsive design et media queries.

.plein-ecran {
  min-height: 100vh;  /* repli pour les navigateurs anciens */
  min-height: 100dvh; /* suit la barre d'adresse sur mobile */
}

Ce que j'utilise, et quand

Voici le récapitulatif que j'aurais aimé lire en commençant. La colonne de droite n'est pas une loi, c'est le choix qui évite le plus de surprises dans un site ordinaire.

UnitéSe réfère àQuand l'utiliser
pxrien, c'est la référencebordures, ombres, petits décalages fixes
emla taille de police de l'élémentrembourrage d'un bouton, espacement lié au texte
remla taille de police de la racinetailles de texte, espacements généraux, points de rupture
%le bloc conteneur, selon la propriétélargeurs fluides, blocs à proportions fixes
nombre seulla taille de police de l'élémentuniquement pour line-height, parmi les propriétés de dimension
vw et vhla fenêtre d'affichagesections plein écran en min-height, avec dvh sur mobile ; titres fluides dans un clamp()

Si un seul réflexe devait rester de cet article, ce serait celui-ci : devant une valeur qui ne donne pas ce que vous attendiez, ne changez pas l'unité au hasard, demandez-vous à quoi elle se réfère, puis vérifiez la valeur calculée dans l'onglet Éléments de votre navigateur. Le panneau des styles affiche la valeur que le navigateur a retenue, et c'est elle qui a raison. Une fois ce réflexe acquis, Flexbox et les mises en page fluides deviennent nettement moins mystérieux, parce qu'on cesse de deviner.