Vincent De Oliveira Retour accueil

CSS avancé : métriques des fontes, line-height et vertical-align

Blog

Lecture : 20min

Article also in [EN] Deep dive CSS: font metrics, line-height and vertical-align

Line-height et vertical-align sont des propriétés très simples. Tellement simples que la majorité d’entre nous sommes persuadés de savoir comment elles fonctionnent et comment les utiliser. Mais non. Elles sont vraiment tout sauf simples, voire les plus complexes, puisqu’elles ont un rôle important dans la création de l’une des choses les moins connues de CSS : le contexte de formatage inline (inline formatting context)

Par exemple, line-height peut être défini comme une longueur ou comme une valeur sans unité 1, mais la valeur par défaut est normal. OK, mais c’est combien normal ? On lit souvent que c’est (ou que ça devrait être) 1, ou alors 1.2, même la spec CSS n’est pas très claire sur ce point. Ce que l’on sait, c’est que le line-height sans unité est relatif à la propriété font-size, mais le problème c’est que font-size: 100px se comporte différemment entre les polices, donc est-ce que line-height est également différent ? Est-ce vraiment entre 1 et 1.2 ? Et vertical-align, quelles sont ses implications par rapport à line-height ?

Parlons taille de fontes avec font-size

Prenons ce simple code HTML, un <p> qui contient trois <span>, chacun avec un font-family différent :

<p>
    <span class="a">Ba</span>
    <span class="b">Ba</span>
    <span class="c">Ba</span>
</p>
p  { font-size: 100px }
.a { font-family: Helvetica }
.b { font-family: Gruppo    }
.c { font-family: Catamaran }

Les éléments ont le même font-size, mais les polices produisent des éléments de hauteurs différentes :

Familles de polices différentes, tailles identiques, donnent des hauteurs différentes

Bien que ce soit un comportement connu, pourquoi font-size: 100px ne génére pas des éléments de 100px de haut ? J’ai donc mesuré et j’obtiens : Helvetica : 115px, Gruppo : 97px et Catamaran : 164px

Des éléments avec font-size: 100px ont des hauteurs qui varient de 97px à 164px

Même si ça semble étrange, c’est tout à fait normal. La raison se trouve au sein de la police elle-même. Voici comment cela fonctionne :

  • une fonte définit son em-square (ou UPM, units per em), une sorte de conteneur où chaque caractère sera dessiné. Cet em-square utilise des unités relatives et vaut généralement 1000 unités. Mais il peut aussi être défini à 1024, 2048 ou toute autre valeur.
  • relativement à cet em-square, les métriques de la fonte sont définies (ascendantes, descendantes, hauteur des capitales, hauteur d’x, etc.). Notez que certaines valeurs peuvent dépasser de l’em-square.
  • au sein du navigateur, les unités relatives sont transformées pour être adaptées à la taille de la police

Prenons par exemple la police Catamaran et ouvrons-là dans FontForge pour obtenir ses métriques :

  • l’em-square est de 1000
  • les ascendantes sont de 1100 et les descendantes de 540. D’après mes tests, il semble que les navigateurs utilisent les champs HHead Ascent/Descent sur Mac OS, et Win Ascent/Descent sur Windows (ces valeurs peuvent être différentes !). On voit également que Capital Height vaut 680 et que X Height vaut 485.
Métriques de fonte dans FontForge

Cela signifie que la police Catamaran utilises 1100 + 540 unités dans un em-square de 1000, ce qui nous donne une hauteur de 164px lorsque font-size: 100px est utilisé. Cette hauteur calculée est le content-area d’un élément et j’utiliserais ce terme en anglais pour le reste de l’article. Vous pouvez assimiler le content-area à la zone où le background s’applique 2.

On peut également voir que les lettres capitales ont une hauteur de 68px (680 unités) et les lettres minuscules (hauteur d’x) une hauteur de 49px (485 unités). Par conséquent, 1ex = 49px et 1em = 100px, pas 164px (heureusement, em est relatif à font-size, pas à la hauteur calculée)

Police Catamaran : UPM —Units Per Em— et équivalents pixel avec font-size: 100px

Avant d’aller plus loin, un petite explication sur ce que cela implique. Quand un élément <p> est affiché à l’écran, il peut être composé de plusieurs lignes, en fonction de sa largeur. Chaque ligne est faite d’un ou plusieurs éléments inline (balises HTML ou éléments en ligne anonymes) et est appelée une line-box. La hauteur d’une line-box est basée sur la hauteur de ses enfants. Le navigateur doit alors calculer la hauteur de chaque enfant et ainsi en déduire la hauteur de la line-box (du point de l’enfant le plus haut au point de l’enfant le plus bas). Une line-box est donc toujours assez grande pour contenir tous ses enfants (par défaut).

Chaque élément HTML est en fait un empilement de line-boxes. Si vous connaissez la hauteur de chaque line-box, vous pouvez connaitre la hauteur de l’élément.

Modifions le code précédent comme ceci :

<p>
    Good design will be better.
    <span class="a">Ba</span>
    <span class="b">Ba</span>
    <span class="c">Ba</span>
    We get to make a consequence.
</p>

Trois line-boxes seront générées :

  • la première et la dernière contiennent chacune un seul élément inline anonyme (contenu texte)
  • la seconde contient deux éléments inline anonymes et les trois <span>
Un <p> (bordure noire) est fait de line-boxes (bordures blanches) qui contiennent des éléments en ligne (bordures solides) et des éléments anonymes (bordures tiretées)

On voit clairement que la seconde line-box est plus haute que les autres, du à la hauteur du content-area de ses enfants, et notamment celui qui utilise la police Catamaran.

Le plus difficile dans la création des line-boxes, c’est que l’on a aucun moyen de les identifier, ni de les contrôler par CSS. Même en appliquant un background sur ::first-line, le résultat visuel n’est pas ce à quoi on s’attends.

line-height : vers les problèmes et au-delà

Jusqu’à maintenant, j’ai évoqué deux notions : content-area et line-box. Si vous avez bien lu, j’ai dit que la hauteur d’une line-box est calculée par rapport à la hauteur de ses enfants, je ne pas dit que c’était par rapport à la hauteur du content-area de ses enfants. Et ça fait une grande différence.

Aussi étrange que cela puisse paraitre, un élément inline a deux hauteurs différentes : la hauteur de son content-area et la hauteur de sa virtual-area (j’ai inventé le terme virtual-area car cette hauteur est invisible pour nous, donc vous ne trouverez aucune occurence dans la spec. J'utiliserais par contre également ce terme en anglais pour l’article)

  • la hauteur du content-area est définie par les métriques de la police (comme vu plus haut)
  • la hauteur de la virtual-area est en fait le line-height, et c’est cette hauteur qui est utilisée pour le calcul de la hauteur des line-boxes
Les éléments inline ont deux hauteurs différentes

Et donc, le mythe de croire que line-height est la distance entre les lignes de bases du texte s’effondre. En CSS, ce n’est pas le cas 3.

En CSS, line-height n’est pas la distance entre les lignes de base du texte

La différence de hauteur entre la virtual-area et le content-area est appelée le leading. La moitié de ce leading est ajouté au-dessus du content-area, l’autre moitié est ajouté en dessous. Le content-area est ainsi toujours au milieu de la virtual-area.

En fonction de sa valeur calculée, le line-height (virtual-area) peut être égal, plus grand ou plus petit que le content-area. Dans le cas où la virtual-area est plus petite, le leading est négatif et la line-box sera alors visuellement plus petite que ses enfants.

Il existe également d’autres types d’éléments inline :

  • éléments inline remplacés (<img>, <input>, <svg>, etc.)
  • éléments inline-block et tous les autres inline-*
  • éléments qui participent à un contexte de formatage spécifique (par exemple, dans un élément flexbox, les flex items, même inline, ont une valeur calculée blocksifiée)

Pour ces éléments inline particuliers, leur hauteur est calculée en fonction de leurs propriétés height, margin et border. Si height est auto, alors c’est line-height qui est utilisée et le content-area est de ce fait égal à line-height.

Les éléments inline remplacés, inline-* et les éléments blocksifiés ont un content-area égal à height, ou line-height

Bref, le problème actuel, c’est toujours de savoir combien vaut line-height: normal ? La réponse, comme pour le calcul du content-area se trouve dans les métriques de notre fonte.

Retournons donc dans FontForge. L'em-square de Catamaran est de 1000, mais on voit plusieurs valeurs pour les ascendantes/descendantes :

  • generals Ascent/Descent : les ascendantes sont de 770 et les descendantes de 230. Utilisées pour le dessin des glyphes. (table «OS/2»)
  • metrics Ascent/Descent : les ascendantes sont de 1100 et les descendantes de 540. Utilisées pour la hauteur du content-area (table «hhea» et «OS/2»)
  • metric Line Gap. Utilisée pour line-height: normal, en ajoutant cette valeur aux métriques Ascent/Descent (table «hhea»)

Dans notre cas, la police Catamaran définit un interligne (Line Gap) de 0, donc line-height: normal sera égal au content-area, qui est de 1640 unités, soit 1.64.

A titre de comparaison, la police Arial décrit un em-square de 2048 unités, des ascendantes de 1854, des descendantes de 434 et un interligne de 67. Cela veut dire que font-size: 100px nous donne un content-area de 112px (1117 unités) et une line-height: normal de 115px (1150 unités ou 1.15). Toutes ces métriques sont spécifiques à chaque police, et définies par le designer de la fonte.

Ça parait donc évident que line-height: 1 est une mauvaise pratique. Je vous rappelle que les valeurs sans unités font référence à la taille de la police, non pas à la taille du content-area, et devoir gérer une virtual-area plus petite que le content-area est la cause de pas mal de problèmes.

Lutilisation de line-height: 1 peut créer une line-box plus petite que le content-area

Mais pas seulement line-height: 1. À titre d’information, j’ai 1117 polices installées sur ma machine (oui, j’ai installé tout Google Web Fonts)), et 1059 polices, soit environ 95%, ont une valeur de line-height calculée plus grande que 1. Toutes les polices ont des valeurs de line-height qui vont de 0.618 à 3.378. Oui, vous avez bien lu, 3.378 !

Quelques détails sur le calcul des line-boxes :

  • pour les éléments inline, padding et border augmentent la zone de l'arrière-plan (background), mais pas la hauteur du content-area (donc ni la hauteur de la line-box). Le content-area n’est donc pas toujours ce que vous voyez à l’écran. margin-top et margin-bottom n’ont pas d’effet
  • pour les éléments inline remplacés, inline-* et éléments inline blocksifiés : padding, margin et border augmentent la hauteur, et donc le content-area et la line-box

vertical-align : une propriété pour les gouverner toutes

Je n’ai pas encore mentionné la propriété vertical-align, bien que ce soit un facteur essentiel dans le calcul de la hauteur d’une line-box. On peut même dire que vertical-align a un rôle majeur dans le contexte de formatage inline

La valeur par défaut est baseline. Vous vous souvenez des métriques ascendantes et descendantes ? Ces valeurs déterminent où la ligne de base (baseline) se situe, et donc le ratio. Comme le ratio entre ascendantes et descendantes est rarement 50/50, des effets inattendus se produisent rapidement, par exemple avec des éléments frères.

Commençons avec ce code :

<p>
    <span>Ba</span>
    <span>Ba</span>
</p>
p {
    font-family: Catamaran;
    font-size: 100px;
    line-height: 200px;
}

Un <p> contient deux <span> qui héritent chacun de font-family, font-size et d’une line-height fixe. Les lignes de base vont coincider et donc la hauteur de la line-box est égale à leur line-height.

Polices et tailles identiques, ligne de base identique, tout est OK

Que se passe t’il si le deuxième <span> a une font-size plus petite ?

span:last-child {
    font-size: 50px;
}

Le résultat est inattendu, mais l’alignement par défaut baseline crée une line-box plus haute (!) que précédemment, comme vous pouvez le voir dans l’image ci-dessous. Je vous rappelle que la hauteur d’une line-box est calculée entre le point de son enfant le plus haut, vers le point de son enfant le plus bas.

Un enfant plus petit peut créer une line-box plus haute

Ça pourrait être un argument en faveur de l’utilisation de valeur sans unité pour line-height, mais dans certains cas vous avez besoin de valeurs fixes pour créer un rhythme vertical parfait. Pour être franc, peu importe ce que vous choisissez, vous aurez toujours des problèmes avec les alignements des éléments en ligne.

Prenons cet autre exemple. Un <p> avec line-height: 200px, qui contient un seul <span> qui hérite de line-height

<p>
    <span>Ba</span>
</p>
p {
    line-height: 200px;
}
span {
    font-family: Catamaran;
    font-size: 100px;
}

Quelle est la hauteur de la line-box ? On s’attendrait à ce que soit 200px, mais non. Le problème ici vient du fait que le <p> a sa propre police (le défaut est serif). Les lignes de base entre le <p> et le <span> sont certainement différentes, et donc la hauteur de la line-box est plus haute que prévue. Ce qui se passe, c’est que les navigateurs font leurs calculs comme si chaque line-box commençait par un caractère d’une largeur de 0px, que la spec appelle le strut

Un caractère insivible, mais un effet bien visible.

Pour faire simple, on retombe exactement sur le même problème que précédemment avec des éléments frères.

Chaque enfant est également aligné par rapport à un caractère invisible d’une largeur de 0px

L’alignement baseline est faussé, alors est-ce que vertical-align: middle serait la solution ? Comme on peut le lire dans la spec, middle «aligne le point du milieu de la hauteur d’une boite avec la ligne de base du parent, plus la moitié de la hauteur d’x du parent». Les ratios des lignes de base sont différents, mais aussi les ratios de hauteur d’x, donc middle n’est pas fiable non plus. Pire, dans la majorité des cas, middle ne positionne pas l’élément au «centre». Trop de facteurs qui ne peuvent pas être modifiés par CSS entrent en jeu (hauteur d’x, ratio ascendantes/descendantes, etc.)

Hormis ces deux là, il existe quatre autres valeurs qui peuvent être utiles dans certains cas :

  • vertical-align: top / bottom alignent sur le haut ou le bas de la line-box
  • vertical-align: text-top / text-bottom alignent sur le haut ou le bas du content-area
Vertical-align: top, bottom, text-top et text-bottom

Faites tout de même attention, car dans tout les cas, c'est la virtual-area qui est alignée, et donc la hauteur invisible. Regardez cet exemple qui utilise vertical-align: top. Le line-height invisible peut générer un résultat étrange, mais en fait tout à fait normal.

vertical-align peut produire un résultat étrange à première vue, mais totalement normal quand on visualise le line-height

Enfin, vertical-align accepte également des valeurs numériques qui font monter ou descendre la boite par rapport à la ligne de base. Cette dernière option peut dans certains cas se révéler intéressante.

CSS is awesome

On a évoqué les interactions entre line-height et vertical-align, et la question que l’on peut se poser, c’est : est-ce que les métriques des fontes sont controllables en CSS ? Pour faire simple : non. Même si j’aimerais tellement que ce soit le cas.

Peu importe, je pense qu’on peut quand même s‘amuser un peu. Les métriques des fontes sont des constantes, on devrait donc pouvoir faire quelque chose.

Est-ce possible, par exemple, de faire en sorte que notre texte qui utilise la police Catamaran ait des lettres capitales de 100px de hauteur ? Ça semble jouable, faisons un peu de maths.

Premièrement, définissons toutes les métriques de notre police comme des propriétés CSS custom (aka CSS variables) 4, et calculons font-size pour avoir une hauteur de lettres capitales de 100px.

p {
    /* font metrics */
    --font: Catamaran;
    --fm-capitalHeight: 0.68;
    --fm-descender: 0.54;
    --fm-ascender: 1.1;
    --fm-linegap: 0;

    /* desired font-size for capital height */
    --capital-height: 100;

    /* apply font-family */
    font-family: var(--font);

    /* compute font-size to get capital height equal desired font-size */
    --computedFontSize: (var(--capital-height) / var(--fm-capitalHeight));
    font-size: calc(var(--computedFontSize) * 1px);
}
Les lettres capitales mesurent 100px de hauteur

Plutôt simple, non ? Bon, si nous voulons maintenant que le texte soit visuellement centré, de sorte que l’espace restant soit identique au-dessus et en-dessous de la lettre «B» ? Pour cela, nous allons calculer le vertical-align, en fonction du ratio ascendantes/descendantes.

Calculons d’abord line-height: normal et la hauteur du content-area :

p {
    …
    --lineheightNormal: (var(--fm-ascender) + var(--fm-descender) + var(--fm-linegap));
    --contentArea: (var(--lineheightNormal) * var(--computedFontSize));
}

Ensuite, nous avons besoin de :

  • la distance entre le bas de la lettre et le bas de la boite
  • la distance entre le haut de la lettre et le haut de la boite

Comme ça :

p {
    …
    --distanceBottom: (var(--fm-descender));
    --distanceTop: (var(--fm-ascender) - var(--fm-capitalHeight));
}

Nous pouvons donc en déduire le vertical-align, qui sera la différence entre ces deux distances, multipliée par la valeur calculée de font-size (cette valeur doit être appliquée à un enfant inline).

p {
    …
    --valign: ((var(--distanceBottom) - var(--distanceTop)) * var(--computedFontSize));
}
span {
    vertical-align: calc(var(--valign) * -1px);
}

Enfin, définissons le line-height souhaité, et calculons la valeur réelle afin de maintenir l’alignement vertical :

p {
    …
    /* desired line-height */
    --line-height: 3;
    line-height: calc(((var(--line-height) * var(--capital-height)) - var(--valign)) * 1px);
}
Résultats avec différents line-height. Le texte est toujours visuellement au centre

Il est maintenant très facile d’ajouter une icone de la même hauteur que la lettre :

span::before {
    content: '';
    display: inline-block;
    width: calc(1px * var(--capital-height));
    height: calc(1px * var(--capital-height));
    margin-right: 10px;
    background: url('https://cdn.pbrd.co/images/yBAKn5bbv.png');
    background-size: cover;
}
L’icone et la lettre B sont de même hauteur

Voir le résultat dans JSBin

Notez quand même que cet exemple est à prendre à titre de démo uniquement. Ce n’est absolument pas une solution fiable. Et ce pour plusieurs raisons :

  • bien que les métriques des fontes soit des constantes, les calculs dans les navigateurs ne le sont pas ¯⁠\⁠(ツ)⁠/⁠¯
  • si la police ne se charge pas, votre solution fallback doit prévoir ce cas et donc éventuellement définir de multiples valeurs de métriques, ce qui devient vite lourd

À emporter

Ce que l’on a appris :

  • le contexte de formatage inline est complexe
  • les éléments inline ont deux hauteurs :
    • le content-area (basé sur les métriques des fontes)
    • la virtual-area (line-height)
    • aucune de ces deux hauteurs ne peut clairement être identifiées
  • line-height: normal est basé sur les métriques de fonte
  • line-height: n peut créer une virtual-area plus petite que le content-area
  • vertical-align n’est pas très fiable
  • la hauteur d’une line-box est calculée en fonction des propriétés line-height et vertical-align de chacun de ses enfants
  • on ne peut pas facilement modifier ces valeurs avec CSS

Mais j’aime toujours CSS :)

Ressources

  1. peu importe le choix que vous faites, ce n'est pas le débat ici
  2. bien que ce ne soit pas tout à fait vrai
  3. dans d’autres logiciels d’édition, ça peut être le cas. Par exemple dans Word ou Photoshop. La différence principale est que la première ligne de texte est également affectée en CSS
  4. on aurait aussi pu utiliser un préprocesseur, les propriétés CSS custom ne sont pas requises ici

Si vous avez aimé cet article, pensez à Flattr !

Animer les couleurs d'une interface