A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.html 33KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang="en">
  4. <!-- Has to be within the first 1024 bytes, hence before the `title` element
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset="utf-8">
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name="viewport" content="width=device-width,initial-scale=1">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>Deep dive CSS: font metrics, line-height and vertical-align (archive) — David Larlet</title>
  13. <meta name="description" content="Publication mise en cache pour en conserver une trace.">
  14. <!-- That good ol' feed, subscribe :). -->
  15. <link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
  16. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  17. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
  18. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
  19. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
  20. <link rel="manifest" href="/static/david/icons2/site.webmanifest">
  21. <link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
  22. <link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
  23. <meta name="msapplication-TileColor" content="#f7f7f7">
  24. <meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
  25. <meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)">
  26. <meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)">
  27. <!-- Is that even respected? Retrospectively? What a shAItshow…
  28. https://neil-clarke.com/block-the-bots-that-feed-ai-models-by-scraping-your-website/ -->
  29. <meta name="robots" content="noai, noimageai">
  30. <!-- Documented, feel free to shoot an email. -->
  31. <link rel="stylesheet" href="/static/david/css/style_2021-01-20.css">
  32. <!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
  33. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  34. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  35. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  36. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  37. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  38. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  39. <script>
  40. function toggleTheme(themeName) {
  41. document.documentElement.classList.toggle(
  42. 'forced-dark',
  43. themeName === 'dark'
  44. )
  45. document.documentElement.classList.toggle(
  46. 'forced-light',
  47. themeName === 'light'
  48. )
  49. }
  50. const selectedTheme = localStorage.getItem('theme')
  51. if (selectedTheme !== 'undefined') {
  52. toggleTheme(selectedTheme)
  53. }
  54. </script>
  55. <meta name="robots" content="noindex, nofollow">
  56. <meta content="origin-when-cross-origin" name="referrer">
  57. <!-- Canonical URL for SEO purposes -->
  58. <link rel="canonical" href="https://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align">
  59. <body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all">
  60. <article>
  61. <header>
  62. <h1>Deep dive CSS: font metrics, line-height and vertical-align</h1>
  63. </header>
  64. <nav>
  65. <p class="center">
  66. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  67. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  68. </svg> Accueil</a> •
  69. <a href="https://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align" title="Lien vers le contenu original">Source originale</a>
  70. <br>
  71. Mis en cache le 2024-04-18
  72. </p>
  73. </nav>
  74. <hr>
  75. <p><code>Line-height</code> and <code>vertical-align</code> are simple CSS properties. So simple that most of us are convinced to fully understand how they work and how to use them. But it’s not. They really are complex, maybe the hardest ones, as <strong>they have a major role in the creation of one of the less-known feature of CSS: inline formatting context</strong>.</p>
  76. <p>For example, <code>line-height</code> can be set as a length or a unitless value , but the default is <code>normal</code>. OK, but what normal is? We often read that it is (or should be) 1, or maybe 1.2, even the <a href="https://www.w3.org/TR/CSS2/visudet.html#propdef-line-height">CSS spec is unclear on that point</a>. We know that unitless <code>line-height</code> is <code>font-size</code> relative, but the problem is that <code>font-size: 100px</code> behaves differently across font-families, so is <code>line-height</code> always the same or different? Is it really between 1 and 1.2? And <code>vertical-align</code>, what are its implications regarding <code>line-height</code>?</p>
  77. <p>Deep dive into a not-so-simple CSS mechanism…</p>
  78. <h2 id="lets-talk-about-font-size-first">Let’s talk about <code>font-size</code> first<a href="#lets-talk-about-font-size-first" class="self-link"></a></h2>
  79. <p>Look at this simple HTML code, a <code>&lt;p&gt;</code> tag containing 3 <code>&lt;span&gt;</code>, each with a different <code>font-family</code>:</p>
  80. <pre class="language-markup"><code>&lt;p&gt;
  81. &lt;span class="a"&gt;Ba&lt;/span&gt;
  82. &lt;span class="b"&gt;Ba&lt;/span&gt;
  83. &lt;span class="c"&gt;Ba&lt;/span&gt;
  84. &lt;/p&gt;</code></pre>
  85. <pre><code>p { font-size: 100px }
  86. .a { font-family: Helvetica }
  87. .b { font-family: Gruppo }
  88. .c { font-family: Catamaran }</code></pre>
  89. <p>Using the same <code>font-size</code> with different font-families produce elements with various heights:</p>
  90. <figure><img src="/content/01-blog/30-css-avance-metriques-des-fontes-line-height-et-vertical-align/font-size.png"><figcaption class="caption">Different font-families, same font-size, give various heights</figcaption></figure>
  91. <p>Even if we’re aware of that behavior, why <code>font-size: 100px</code> does not create elements with 100px height? I’ve measured and found these values: Helvetica: 115px, Gruppo: 97px and Catamaran: 164px</p>
  92. <figure><img src="/content/01-blog/30-css-avance-metriques-des-fontes-line-height-et-vertical-align/font-size-line-height.png"><figcaption class="caption">Elements with font-size: 100px have height that varies from 97px to 164px</figcaption></figure>
  93. <p>Although it seems a bit weird at first, it’s totally expected. <strong>The reason lays down inside the font itself</strong>. Here is how it works:</p>
  94. <ul>
  95. <li>a font defines its <a href="http://designwithfontforge.com/en-US/The_EM_Square.html">em-square</a> (or UPM, units per em), a kind of container where each character will be drawn. This square uses relative units and is generally set at 1000 units. But it can also be 1024, 2048 or anything else.</li>
  96. <li>based on its relative units, metrics of the fonts are set (ascender, descender, capital height, x-height, etc.). Note that some values can bleed outside of the em-square.</li>
  97. <li>in the browser, relative units are scaled to fit the desired font-size.</li>
  98. </ul>
  99. <p>Let’s take the Catamaran font and open it in <a href="https://fontforge.github.io/en-US/">FontForge</a> to get metrics:</p>
  100. <ul>
  101. <li>the em-square is 1000</li>
  102. <li>the ascender is 1100 and the descender is 540. After running some tests, it seems that browsers use the <em>HHead Ascent</em>/<em>Descent</em> values on Mac OS, and <em>Win Ascent</em>/<em>Descent</em> values on Windows (these values may differ!). We also note that <em>Capital Height</em> is 680 and <em>X height</em> is 485.</li>
  103. </ul>
  104. <figure><img src="/content/01-blog/30-css-avance-metriques-des-fontes-line-height-et-vertical-align/font-forge-metrics.png"><figcaption class="caption">Font metrics values using FontForge</figcaption></figure>
  105. <p>That means the Catamaran font uses 1100 + 540 units in a 1000 units em-square, which gives a height of 164px when setting <code>font-size: 100px</code>. <strong>This computed height defines the <em>content-area</em> of an element</strong> and I will refer to this terminology for the rest of the article. You can think of the <em>content-area</em> as the area where the <code>background</code> property applies .</p>
  106. <p>We can also predict that capital letters are 68px high (680 units) and lower case letters (x-height) are 49px high (485 units). As a result, <code>1ex</code> = 49px and <code>1em</code> = 100px, not 164px (thankfully, <code>em</code> is based on <code>font-size</code>, not computed height)</p>
  107. <figure><img src="/content/01-blog/30-css-avance-metriques-des-fontes-line-height-et-vertical-align/upm-px-equivalent.png"><figcaption class="caption">Catamaran font: UPM —Units Per Em— and pixels equivalent using font-size: 100px</figcaption></figure>
  108. <p>Before going deeper, a short note on what this it involves. When a <code>&lt;p&gt;</code> element is rendered on screen, it can be composed of many lines, according to its width. Each line is made up of one or many inline elements (HTML tags or anonymous inline elements for text content) and is called a <em>line-box</em>. <strong>The height of a <em>line-box</em> is based on its children’s height</strong>. The browser therefore computes the height for each inline elements, and thus the height of the <em>line-box</em> (from its child’s highest point to its child’s lowest point). As a result, a <em>line-box</em> is always tall enough to contain all its children (by default).</p>
  109. <blockquote>
  110. <p>Each HTML element is actually a stack of <em>line-boxes</em>. If you know the height of each <em>line-box</em>, you know the height of an element.</p>
  111. </blockquote>
  112. <p>If we update the previous HTML code like this:</p>
  113. <pre class="language-markup"><code>&lt;p&gt;
  114. Good design will be better.
  115. &lt;span class="a"&gt;Ba&lt;/span&gt;
  116. &lt;span class="b"&gt;Ba&lt;/span&gt;
  117. &lt;span class="c"&gt;Ba&lt;/span&gt;
  118. We get to make a consequence.
  119. &lt;/p&gt;</code></pre>
  120. <p>It will generate 3 <em>line-boxes</em>:</p>
  121. <ul>
  122. <li>the first and last one each contain a single anonymous inline element (text content)</li>
  123. <li>the second one contains two anonymous inline elements, and the 3 <code>&lt;span&gt;</code></li>
  124. </ul>
  125. <figure><img src="/content/01-blog/30-css-avance-metriques-des-fontes-line-height-et-vertical-align/line-boxes.png"><figcaption class="caption">A <code>&lt;p&gt;</code> (black border) is made of line-boxes (white borders) that contain inline elements (solid borders) and anonymous inline elements (dashed borders)</figcaption></figure>
  126. <p>We clearly see that the second <em>line-box</em> is taller than the others, due to the computed <em>content-area</em> of its children, and more specifically, the one using the Catamaran font.</p>
  127. <p><strong>The difficult part in the <em>line-box</em> creation is that we can’t really see, nor control it with CSS</strong>. Even applying a background to <code>::first-line</code> does not give us any visual clue on the first <em>line-box</em>’s height.</p>
  128. <h2 id="line-height-problems-and-beyond"><code>line-height</code>: to the problems and beyond<a href="#line-height-problems-and-beyond" class="self-link"></a></h2>
  129. <p>Until now, I introduced two notions: <em>content-area</em> and <em>line-box</em>. If you’ve read it well, I told that a <em>line-box</em>’s height is computed according to its children’s height, I didn’t say its children <em>content-area</em>’s height. And that makes a big difference.</p>
  130. <p>Even though it may sound strange, <strong>an inline element has two different height: the <em>content-area</em> height and the <em>virtual-area</em> height</strong> (I invented the term <em>virtual-area</em> as the height is invisible to us, but you won’t find any occurrence in the spec).</p>
  131. <ul>
  132. <li>the <em>content-area</em> height is defined by the font metrics (as seen before)</li>
  133. <li><strong>the <em>virtual-area</em> height is the <code>line-height</code></strong>, and it is the height <strong>used to compute the <em>line-box</em>’s height</strong></li>
  134. </ul>
  135. <figure><img src="/content/01-blog/30-css-avance-metriques-des-fontes-line-height-et-vertical-align/line-height.png"><figcaption class="caption">Inline elements have two different height</figcaption></figure>
  136. <p>That being said, it breaks down the popular belief that <code>line-height</code> is the distance between baselines. In CSS, it is not .</p>
  137. <figure><img src="/content/01-blog/30-css-avance-metriques-des-fontes-line-height-et-vertical-align/line-height-yes-no.png"><figcaption class="caption">In CSS, the line-height is not the distance between baselines</figcaption></figure>
  138. <p>The computed difference of height between the <em>virtual-area</em> and the <em>content-area</em> is called the leading. Half this leading is added on top of the <em>content-area</em>, the other half is added on the bottom. <strong>The <em>content-area</em> is therefore always on the middle of the <em>virtual-area</em></strong>.</p>
  139. <p>Based on its computed value, the <code>line-height</code> (<em>virtual-area</em>) can be equal, taller or smaller than the <em>content-area</em>. In case of a smaller <em>virtual-area</em>, leading is negative and a <em>line-box</em> is visually smaller than its children.</p>
  140. <p>There are also other kind of inline elements:</p>
  141. <ul>
  142. <li>replaced inline elements (<code>&lt;img&gt;</code>, <code>&lt;input&gt;</code>, <code>&lt;svg&gt;</code>, etc.)</li>
  143. <li><code>inline-block</code> and all <code>inline-*</code> elements</li>
  144. <li>inline elements that participate in a specific formatting context (eg. in a flexbox element, all flex items are <em>blocksified</em>)</li>
  145. </ul>
  146. <p>For these specific inline elements, height is computed based on their <code>height</code>, <code>margin</code> and <code>border</code> properties. If <code>height</code> is <code>auto</code>, then <code>line-height</code> is used and the <em>content-area</em> is strictly equal to the <code>line-height</code>.</p>
  147. <figure><img src="/content/01-blog/30-css-avance-metriques-des-fontes-line-height-et-vertical-align/line-height-inline-block.png"><figcaption class="caption">Inline replaced elements, inline-block/inline-* and blocksified inline elements have a content-area equal to their height, or line-height</figcaption></figure>
  148. <p>Anyway, the problem we’re still facing is how much the <code>line-height</code>’s <code>normal</code> value is? And the answer, as for the computation of the <em>content-area</em>’s height, is to be found inside the font metrics.</p>
  149. <p>So let’s go back to FontForge. The Catamaran’s em-square is 1000, but we’re seeing many ascender/descender values:</p>
  150. <ul>
  151. <li>generals <em>Ascent/Descent</em>: ascender is 770 and descender is 230. Used for character drawings. (table <em>“OS/2”</em>)</li>
  152. <li>metrics <em>Ascent/Descent</em>: ascender is 1100 and descender is 540. Used for <em>content-area</em>’s height. (table <em>“hhea”</em> and table <em>“OS/2”</em>)</li>
  153. <li>metric <em>Line Gap</em>. Used for <code>line-height: normal</code>, by adding this value to <em>Ascent/Descent</em> metrics. (table <em>“hhea”</em>)</li>
  154. </ul>
  155. <p>In our case, the Catamaran font defines a 0 unit line gap, so <strong><code>line-height: normal</code> will be equal to the <em>content-area</em>, which is 1640 units, or 1.64</strong>.</p>
  156. <p>As a comparison, the Arial font describes an em-square of 2048 units, an ascender of 1854, a descender of 434 and a line gap of 67. It means that <code>font-size: 100px</code> gives a <em>content-area</em> of 112px (1117 units) and a <code>line-height: normal</code> of 115px (1150 units or 1.15). All these metrics are font-specific, and set by the font designer.</p>
  157. <p><strong>It becomes obvious that setting <code>line-height: 1</code> is a bad practice</strong>. I remind you that unitless values are <code>font-size</code> relative, not <em>content-area</em> relative, and dealing with a <em>virtual-area</em> smaller than the <em>content-area</em> is the origin of many of our problems.</p>
  158. <figure><img src="/content/01-blog/30-css-avance-metriques-des-fontes-line-height-et-vertical-align/line-height-1.png"><figcaption class="caption">Using line-height: 1 can create a line-box smaller than the content-area</figcaption></figure>
  159. <p>But not only <code>line-height: 1</code>. For what it’s worth, on the 1117 fonts installed on my computer (yes, <a href="https://github.com/qrpike/Web-Font-Load">I installed all fonts from Google Web Fonts</a>), 1059 fonts, around 95%, have a computed <code>line-height</code> greater than 1. Their computed <code>line-height</code> goes from 0.618 to 3.378. You’ve read it well, 3.378!</p>
  160. <p>Small details on <em>line-box</em> computation:</p>
  161. <ul>
  162. <li>for inline elements, <code>padding</code> and <code>border</code> increases the background area, but not the <em>content-area</em>’s height (nor the <em>line-box</em>’s height). The <em>content-area</em> is therefore not always what you see on screen. <code>margin-top</code> and <code>margin-bottom</code> have no effect.</li>
  163. <li>for replaced inline elements, <code>inline-block</code> and <em>blocksified</em> inline elements: <code>padding</code>, <code>margin</code> and <code>border</code> increases the <code>height</code>, so the <em>content-area</em> and <em>line-box</em>’s height</li>
  164. </ul>
  165. <h2 id="vertical-align-one-property-rule-them-all"><code>vertical-align</code>: one property to rule them all<a href="#vertical-align-one-property-rule-them-all" class="self-link"></a></h2>
  166. <p>I didn’t mention the <code>vertical-align</code> property yet, even though it is an essential factor to compute a <em>line-box</em>’s height. We can even say that <strong><code>vertical-align</code> may have the leading role in inline formatting context</strong>.</p>
  167. <p>The default value is <code>baseline</code>. Do you remind font metrics ascender and descender? These values determine where the baseline stands, and so the ratio. As the ratio between ascenders and descenders is rarely 50/50, it may produce unexpected results, for example with siblings elements.</p>
  168. <p>Start with that code:</p>
  169. <pre class="language-markup"><code>&lt;p&gt;
  170. &lt;span&gt;Ba&lt;/span&gt;
  171. &lt;span&gt;Ba&lt;/span&gt;
  172. &lt;/p&gt;</code></pre>
  173. <pre><code>p {
  174. font-family: Catamaran;
  175. font-size: 100px;
  176. line-height: 200px;
  177. }</code></pre>
  178. <p>A <code>&lt;p&gt;</code> tag with 2 siblings <code>&lt;span&gt;</code> inheriting <code>font-family</code>, <code>font-size</code> and fixed <code>line-height</code>. Baselines will match and the <em>line-box</em>’s height is equal to their <code>line-height</code>.</p>
  179. <figure><img src="/content/01-blog/30-css-avance-metriques-des-fontes-line-height-et-vertical-align/vertical-align-baseline.png"><figcaption class="caption">Same font values, same baselines, everything seems OK</figcaption></figure>
  180. <p>What if the second element has a smaller <code>font-size</code>?</p>
  181. <pre><code>span:last-child {
  182. font-size: 50px;
  183. }</code></pre>
  184. <p>As strange as it sounds, <strong>default baseline alignment may result in a higher (!) <em>line-box</em></strong>, as seen in the image below. I remind you that a <em>line-box</em>’s height is computed from its child’s highest point to its child’s lowest point.</p>
  185. <figure><img src="/content/01-blog/30-css-avance-metriques-des-fontes-line-height-et-vertical-align/vertical-align-baseline-nok.png"><figcaption class="caption">A smaller child element may result in a higher line-box's height</figcaption></figure>
  186. <p>That could be <a href="http://allthingssmitty.com/2017/01/30/nope-nope-nope-line-height-is-unitless/">an argument in favor of using <code>line-height</code> unitless values</a>, but sometimes you need fixed ones to <a href="https://scotch.io/tutorials/aesthetic-sass-3-typography-and-vertical-rhythm#baseline-grids-and-vertical-rhythm">create a perfect vertical rhythm</a>. <strong>To be honest, no matter what you choose, you’ll always have trouble with inline alignments</strong>.</p>
  187. <p>Look at this another example. A <code>&lt;p&gt;</code> tag with <code>line-height: 200px</code>, containing a single <code>&lt;span&gt;</code> inheriting <code>line-height</code></p>
  188. <pre class="language-markup"><code>&lt;p&gt;
  189. &lt;span&gt;Ba&lt;/span&gt;
  190. &lt;/p&gt;</code></pre>
  191. <pre><code>p {
  192. line-height: 200px;
  193. }
  194. span {
  195. font-family: Catamaran;
  196. font-size: 100px;
  197. }</code></pre>
  198. <p>How high is the <em>line-box</em>? We should expect 200px, but it’s not what we get. The problem here is that the <code>&lt;p&gt;</code> has its own, different <code>font-family</code> (default to <code>serif</code>). Baselines between the <code>&lt;p&gt;</code> tag and the <code>&lt;span&gt;</code> are likely to be different, the height of the <em>line-box</em> is therefore higher than expected. <strong>This happens because browsers do their computation as if each <em>line-box</em> starts with a zero-width character</strong>, that the spec called a strut.</p>
  199. <blockquote>
  200. <p>An invisible character, but a visible impact.</p>
  201. </blockquote>
  202. <p>To resume, we’re facing the same previous problem as for siblings elements.</p>
  203. <figure><img src="/content/01-blog/30-css-avance-metriques-des-fontes-line-height-et-vertical-align/vertical-align-strut.png"><figcaption class="caption">Each child is aligned as if its line-box starts with an invisible zero-width character</figcaption></figure>
  204. <p>Baseline alignment is screwed, but what about <code>vertical-align: middle</code> to the rescue? As you can read in the spec, <code>middle</code> “aligns the vertical midpoint of the box with the baseline of the parent box plus half the x-height of the parent”. <strong>Baselines ratio are different, as well as x-height ratio, so <code>middle</code> alignment isn’t reliable either</strong>. Worst, in most scenarios, <code>middle</code> is never really “at the middle”. Too many factors are involved and cannot be set via CSS (x-height, ascender/descender ratio, etc.)</p>
  205. <p>As a side note, there are 4 other values, that may be useful in some cases:</p>
  206. <ul>
  207. <li><code>vertical-align: top</code> / <code>bottom</code> align to the top or the bottom of the <em>line-box</em></li>
  208. <li><code>vertical-align: text-top</code> / <code>text-bottom</code> align to the top or the bottom of the <em>content-area</em></li>
  209. </ul>
  210. <figure><img src="/content/01-blog/30-css-avance-metriques-des-fontes-line-height-et-vertical-align/vertical-align-top-bottom-text.png"><figcaption class="caption">Vertical-align: top, bottom, text-top and text-bottom</figcaption></figure>
  211. <p>Be careful though, in all cases, it aligns the <em>virtual-area</em>, so the invisible height. Look at this simple example using <code>vertical-align: top</code>. <strong>Invisible <code>line-height</code> may produce odd, but unsurprising, results</strong>.</p>
  212. <figure><img src="/content/01-blog/30-css-avance-metriques-des-fontes-line-height-et-vertical-align/vertical-align-top-virtual-height.png"><figcaption class="caption">vertical-align may produce odd result at first, but expected when visualizing line-height</figcaption></figure>
  213. <p>Finally, <code>vertical-align</code> also accepts numerical values which raise or lower the box regarding to the baseline. That last option could come in handy.</p>
  214. <h2 id="css-awesome">CSS is awesome<a href="#css-awesome" class="self-link"></a></h2>
  215. <p>We’ve talked about how <code>line-height</code> and <code>vertical-align</code> work together, but now the question is: are font metrics controllable with CSS? Short answer: no. Even if I really hope so.
  216. Anyway, I think we have to play a bit. Font metrics are constant, so we should be able to do something.</p>
  217. <p>What if, for example, we want a text using the Catamaran font, where the capital height is exactly 100px high? Seems doable: let’s do some maths.</p>
  218. <p>First we set all font metrics as CSS custom properties , then compute <code>font-size</code> to get a capital height of 100px.</p>
  219. <pre><code>p {
  220. /* font metrics */
  221. --font: Catamaran;
  222. --fm-capitalHeight: 0.68;
  223. --fm-descender: 0.54;
  224. --fm-ascender: 1.1;
  225. --fm-linegap: 0;
  226. /* desired font-size for capital height */
  227. --capital-height: 100;
  228. /* apply font-family */
  229. font-family: var(--font);
  230. /* compute font-size to get capital height equal desired font-size */
  231. --computedFontSize: (var(--capital-height) / var(--fm-capitalHeight));
  232. font-size: calc(var(--computedFontSize) * 1px);
  233. }</code></pre>
  234. <figure><img src="/content/01-blog/30-css-avance-metriques-des-fontes-line-height-et-vertical-align/css-metrics-capital-height.png"><figcaption class="caption">The capital height is now 100px high</figcaption></figure>
  235. <p>Pretty straightforward, isn’t it? But what if we want the text to be visually at the middle, so that the remaining space is equally distributed on top and bottom of the “B” letter? To achieve that, we have to compute <code>vertical-align</code> based on ascender/descender ratio.</p>
  236. <p>First, compute <code>line-height: normal</code> and <em>content-area</em>’s height:</p>
  237. <pre><code>p {
  238. --lineheightNormal: (var(--fm-ascender) + var(--fm-descender) + var(--fm-linegap));
  239. --contentArea: (var(--lineheightNormal) * var(--computedFontSize));
  240. }</code></pre>
  241. <p>Then, we need:</p>
  242. <ul>
  243. <li>the distance from the bottom of the capital letter to the bottom edge</li>
  244. <li>the distance from the top of the capital letter to the top edge</li>
  245. </ul>
  246. <p>Like so:</p>
  247. <pre><code>p {
  248. --distanceBottom: (var(--fm-descender));
  249. --distanceTop: (var(--fm-ascender) - var(--fm-capitalHeight));
  250. }</code></pre>
  251. <p>We can now compute <code>vertical-align</code>, which is the difference between the distances multiplied by the computed <code>font-size</code>. (we must apply this value to an inline child element)</p>
  252. <pre><code>p {
  253. --valign: ((var(--distanceBottom) - var(--distanceTop)) * var(--computedFontSize));
  254. }
  255. span {
  256. vertical-align: calc(var(--valign) * -1px);
  257. }</code></pre>
  258. <p>At the end, we set the desired <code>line-height</code> and compute it while maintaining a vertical alignment:</p>
  259. <pre><code>p {
  260. /* desired line-height */
  261. --line-height: 3;
  262. line-height: calc(((var(--line-height) * var(--capital-height)) - var(--valign)) * 1px);
  263. }</code></pre>
  264. <figure><img src="/content/01-blog/30-css-avance-metriques-des-fontes-line-height-et-vertical-align/css-metrics-results-line-height.png"><figcaption class="caption">Results with different line-height. The text is always on the middle</figcaption></figure>
  265. <p>Adding an icon whose height is matching the letter “B” is now easy:</p>
  266. <pre><code>span::before {
  267. content: '';
  268. display: inline-block;
  269. width: calc(1px * var(--capital-height));
  270. height: calc(1px * var(--capital-height));
  271. margin-right: 10px;
  272. background: url('https://cdn.pbrd.co/images/yBAKn5bbv.png');
  273. background-size: cover;
  274. }</code></pre>
  275. <figure><img src="/content/01-blog/30-css-avance-metriques-des-fontes-line-height-et-vertical-align/css-metrics-results-icon.png"><figcaption class="caption">Icon and B letter are the same height</figcaption></figure>
  276. <p><a href="http://jsbin.com/tufatir/edit?css,output">See result in JSBin</a></p>
  277. <p>Note that this test is for demonstration purpose only. You can’t rely on this. Many reasons:</p>
  278. <ul>
  279. <li>unless font metrics are constant, <a href="https://www.brunildo.org/test/normal-lh-plot.html">computations in browsers are not</a> ¯⁠\<em>⁠(ツ)⁠</em>/⁠¯</li>
  280. <li>if font is not loaded, fallback font has probably different font metrics, and dealing with multiple values will quickly become quite unmanageable</li>
  281. </ul>
  282. <h2 id="takeaways">Takeaways<a href="#takeaways" class="self-link"></a></h2>
  283. <p>What we learned:</p>
  284. <ul>
  285. <li>inline formatting context is really hard to understand</li>
  286. <li>all inline elements have 2 height:
  287. <ul>
  288. <li>the <em>content-area</em> (based on font metrics)</li>
  289. <li>the <em>virtual-area</em> (<code>line-height</code>)</li>
  290. <li>none of these 2 heights can be visualize with no doubt. (if you're a devtools developer and want to work on this, it could be awesome)</li>
  291. </ul></li>
  292. <li><code>line-height: normal</code> is based on font metrics</li>
  293. <li><code>line-height: n</code> may create a <em>virtual-area</em> smaller than <em>content-area</em></li>
  294. <li><code>vertical-align</code> is not very reliable</li>
  295. <li>a <em>line-box</em>’s height is computed based on its children’s <code>line-height</code> and <code>vertical-align</code> properties</li>
  296. <li>we cannot easily get/set font metrics with CSS</li>
  297. </ul>
  298. <p>But I still love CSS :)</p>
  299. </article>
  300. <hr>
  301. <footer>
  302. <p>
  303. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  304. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  305. </svg> Accueil</a> •
  306. <a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2">
  307. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use>
  308. </svg> Suivre</a> •
  309. <a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie">
  310. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use>
  311. </svg> Pro</a> •
  312. <a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail">
  313. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use>
  314. </svg> Email</a> •
  315. <abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2">
  316. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use>
  317. </svg> Légal</abbr>
  318. </p>
  319. <template id="theme-selector">
  320. <form>
  321. <fieldset>
  322. <legend><svg class="icon icon-brightness-contrast">
  323. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use>
  324. </svg> Thème</legend>
  325. <label>
  326. <input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
  327. </label>
  328. <label>
  329. <input type="radio" value="dark" name="chosen-color-scheme"> Foncé
  330. </label>
  331. <label>
  332. <input type="radio" value="light" name="chosen-color-scheme"> Clair
  333. </label>
  334. </fieldset>
  335. </form>
  336. </template>
  337. </footer>
  338. <script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script>
  339. <script>
  340. function loadThemeForm(templateName) {
  341. const themeSelectorTemplate = document.querySelector(templateName)
  342. const form = themeSelectorTemplate.content.firstElementChild
  343. themeSelectorTemplate.replaceWith(form)
  344. form.addEventListener('change', (e) => {
  345. const chosenColorScheme = e.target.value
  346. localStorage.setItem('theme', chosenColorScheme)
  347. toggleTheme(chosenColorScheme)
  348. })
  349. const selectedTheme = localStorage.getItem('theme')
  350. if (selectedTheme && selectedTheme !== 'undefined') {
  351. form.querySelector(`[value="${selectedTheme}"]`).checked = true
  352. }
  353. }
  354. const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
  355. window.addEventListener('load', () => {
  356. let hasDarkRules = false
  357. for (const styleSheet of Array.from(document.styleSheets)) {
  358. let mediaRules = []
  359. for (const cssRule of styleSheet.cssRules) {
  360. if (cssRule.type !== CSSRule.MEDIA_RULE) {
  361. continue
  362. }
  363. // WARNING: Safari does not have/supports `conditionText`.
  364. if (cssRule.conditionText) {
  365. if (cssRule.conditionText !== prefersColorSchemeDark) {
  366. continue
  367. }
  368. } else {
  369. if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
  370. continue
  371. }
  372. }
  373. mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
  374. }
  375. // WARNING: do not try to insert a Rule to a styleSheet you are
  376. // currently iterating on, otherwise the browser will be stuck
  377. // in a infinite loop…
  378. for (const mediaRule of mediaRules) {
  379. styleSheet.insertRule(mediaRule.cssText)
  380. hasDarkRules = true
  381. }
  382. }
  383. if (hasDarkRules) {
  384. loadThemeForm('#theme-selector')
  385. }
  386. })
  387. </script>
  388. </body>
  389. </html>