Skip to content

Chapter III

Line height

How line height tightens as the type grows, why longer lines need more of it, and how to put it on a grid.

  • Line height depends on several factors.
  • line-height: normal comes from the font, not a fixed number.
  • Keep the space between lines the same size as the type grows.
  • Basing line height on line length is experimental.
  • Think twice before aligning to a grid.

The usual advice is to set line-height as a unitless multiple of the font size, with Headings tighter (smaller) than Body text:

h1 {
line-height: 1.2;
}
p {
line-height: 1.5;
}

This makes sense. Headings are larger, so they read well with tighter lines, while smaller text needs more space to stay legible. But the length of the line, or measure, matters too. The longer the line, the further the eye travels back to find the start of the next one, so longer lines need more line height to make that easy. WCAG doesn’t connect the two, but the USWDS does:

Longer texts require more line height. Headings and other content elements no longer than a line or two can have a line height between 1 and 1.35. Longer texts should have a line height of at least 1.5.

Other sources that connect line length and line height:

Important

WCAG 1.4.12 (Text Spacing) doesn’t set a minimum line height for you to use. Instead, it asks that no content or function is lost when a reader replaces your spacing with their own, up to:

  • Line height of 1.5 times the font size.
  • Space after paragraphs of 2 times the font size.
  • Letter spacing of 0.12 times the font size.
  • Word spacing of 0.16 times the font size.

So the tighter line heights this chapter works out for Headings are fine, as long as the page still works when a reader overrides them. Avoid fixed heights on anything that holds text, and size anything that counts lines with the lh unit, which follows the reader’s line height.

Line height also depends on the font itself, through its metrics: the measurements that set where each letter sits within the em-square. So if your Headings and Body use different fonts, they’ll likely need slightly different line heights, even at the same size.

With so many factors, can we write one formula that takes them all into account and gives a good line height every time?

You might think line-height: normal means 1.5. In fact, it comes from the font’s metrics. For the details, read Deep dive CSS: font metrics, line-height and vertical-align by Vincent De Oliveira. Fun fact: he found normal line heights ranging from 0.618 to 3.378 across Google Fonts!

In short, the browser calculates normal like this:

normal=Ascender−Descender+LineGapUnitsPerEm\text{normal} = \frac{\text{Ascender} - \text{Descender} + \text{LineGap}}{\text{UnitsPerEm}}
  • Ascender and descender are the designer’s recommended limits above and below the baseline. The descender is stored as a negative number, which is why it’s subtracted.
  • Line gap is the extra breathing room the designer wants between lines.
  • Units per em is the size of the em-square in the font’s own units, often 1000 or 2048. Dividing by it turns the metrics into a multiple of the font size, which is what line-height expects.
Note

Fonts store these metrics in more than one table (hhea, OS/2 typo, and OS/2 win), and the values can differ between them. Which one a browser uses depends on the font and the operating system. A font that sets the USE_TYPO_METRICS flag asks for the typo values. If your result doesn’t match what you see on screen, check the other tables.

The only online tool I’ve found that shows all of these metrics is FontDrop. Upload your font, scroll down to just above the table of glyphs, and click the “Data” tab.

Think of normal as the line height the designer intended for text at reading size. Our job is to work out what should happen to it as the text gets bigger.

Note

If you use text-box to trim the extra space above the first line and below the last (eg., text-box: cap alphabetic), these calculations still apply. Trimming only affects the outer edges of a block, not the space between lines. It changes how text sits next to other elements, not its line height.

The metrics that make up normal don’t all do the same job. Some of the height is filled by letters, and some is space the designer set aside. We’ll call these glyph height and visual leading.

The glyph height is the band where letters on neighboring lines can touch. It runs from the top of a capital letter, set by the font’s CapHeight\text{CapHeight}, down to the bottom of a descender, the tail on letters like g and p. If the line height is ever smaller than this, a capital on one line will run into a descender on the line above:

glyph=CapHeight−DescenderUnitsPerEm\text{glyph} = \frac{\text{CapHeight} - \text{Descender}}{\text{UnitsPerEm}}

The visual leading is the white space a reader sees between lines: from the bottom of a descender on one line to the top of a capital on the next. In formulas, we’ll write it as ℓ\ell. At any line height, it’s whatever is left over after the glyph height. At normal, it has two parts. The space between the cap height and the ascender is kept free for accents and flourishes, and the line gap is pure breathing room:

ℓ=normal−glyph=Ascender−CapHeight+LineGapUnitsPerEm\begin{aligned} &\ell = \text{normal} - \text{glyph} \\ &\quad = \frac{\text{Ascender} - \text{CapHeight} + \text{LineGap}}{\text{UnitsPerEm}} \end{aligned}

In print, leading has always meant the space between lines. The name comes from the strips of lead that typesetters placed between rows of metal type.

CSS uses the word more narrowly. In CSS, a font’s content area runs from the ascender to the descender. CSS leading is only the extra space the line height adds beyond that, split evenly above and below each line as half-leading. At normal, CSS leading is exactly the line gap.

Visual leading is CSS leading plus the part of the content area above the capitals. It’s the same space that text-box-edge: cap text trims from the edges of a block: the half-leading and the room above the capitals at the top, and the half-leading at the bottom. Once that space is gone, what’s left between two lines is the glyph height.

Added together, the two parts equal normal exactly, so nothing is lost by splitting it. For EB Garamond, the metrics are:

  • Ascender=1007\text{Ascender} = 1007
  • Descender=−298\text{Descender} = -298
  • LineGap=0\text{LineGap} = 0
  • CapHeight=650\text{CapHeight} = 650
  • UnitsPerEm=1000\text{UnitsPerEm} = 1000

That gives:

glyph=650+2981000=0.948\text{glyph} = \frac{650 + 298}{1000} = 0.948 ℓ=1007−650+01000=0.357\ell = \frac{1007 - 650 + 0}{1000} = 0.357 normal=0.948+0.357=1.305\text{normal} = 0.948 + 0.357 = 1.305

Normal is the line height the designer intended for text at reading size, so it suits the smallest size in our scale: Detail text, at 1rem. It’s the loosest line height we’ll use. Body and Heading text are larger, and they need a smaller ratio. To see why, look at what happens when the ratio stays the same.

line-height is a ratio to the font size. Keep the same value as the type grows, and everything it measures grows too, including the space between lines. For EB Garamond, normal at 16px leaves (1.305−0.948)×16=5.7 px(1.305 - 0.948) \times 16 = 5.7\,\text{px} between lines. At 24px, the same 1.305 leaves 8.6 px8.6\,\text{px}. The lines drift apart, even though 5.7px was already enough to separate them.

Splitting normal tells us which part should change. The glyph height belongs to the letters. As the letters grow, it has to grow with them, or the lines will collide. The visual leading is the space between lines, and its job doesn’t change with the size of the type: it separates one line from the next. The Detail size shows how much space does that job, so we keep that space the same size in pixels at every font size.

That’s why the ratio has to be reduced. The letters stay the same amount of the font size, but a space that stays the same size becomes smaller in pixels due to the larger font size. At the Detail size, smin⁡s_{\min}, the space is ℓ×smin⁡\ell \times s_{\min}. As an amount of any font size ss, it’s that divided by ss. Add the height of the letters, and we have the line height as a multiple of the font size, f(s)f(s):

f(s)=glyph+ℓ×smin⁡sf(s) = \text{glyph} + \ell \times \frac{s_{\min}}{s}

At the Detail size, smin⁡s\frac{s_{\min}}{s} is 11, so the multiple is normal. As the type size increases, the fraction gets smaller, and so does the multiple. It gets closer toward the glyph height but never reaches it, so the lines won’t collide.

This is what the usual advice at the start of this chapter does, too. A 16px paragraph at 1.5 has 8.8 px8.8\,\text{px} between its lines. A 40px Heading at the same 1.5 would have 22.1 px22.1\,\text{px}. At 1.2, it’s back to 10.1 px10.1\,\text{px}, close to the paragraph’s. The usual advice keeps the space between lines about the same size. We often guess the ratio, where formulas will work it out from the font metrics.

For EB Garamond, with Detail text at 16px and a Heading at 24px:

f(24)=0.948+0.357×1624≈1.186f(24) = 0.948 + 0.357 \times \frac{16}{24} \approx 1.186

The letters take up 0.948×24=22.8 px0.948 \times 24 = 22.8\,\text{px}, and the space between lines stays at 5.7 px5.7\,\text{px}.

The graph doesn’t decrease in a straight line. It drops fastest just above the Detail size and levels off as the type size increases, because the same space is a smaller amount of each larger font size.

Here’s how the line height changes. You’ll see that the space in pixels stays the same while the letter size grows:

Font size Letters Space between lines Line-height
16px 15.2px 5.7px 1.3051.305
24px 22.8px 5.7px 1.1861.186
36px 34.1px 5.7px 1.1071.107
48px 45.5px 5.7px 1.0671.067
60px 56.9px 5.7px 1.0431.043

Each of the font designer’s choices changes how far the ratio drops:

  • A taller ascender or a larger line gap means more visual leading. Detail text starts looser, and the ratio has further to fall.
  • A taller cap height or a deeper descender means a taller glyph height. The ratio starts closer to the bottom, so the Heading’s lines can’t get as tight.
  • A line gap of 0, which is common, still leaves some visual leading. The space above the cap height still sets the space between lines.
Important

Treating the space above the capitals as visual leading assumes your text doesn’t use it. Accented capitals (eg., Á, Ö) and stacked accents, as in Vietnamese, fill that space. In that case, it should grow with the letters like the rest of the glyph height. Use Ascender−DescenderUnitsPerEm\frac{\text{Ascender} - \text{Descender}}{\text{UnitsPerEm}} for the glyph height instead, which leaves only the line gap as visual leading. For EB Garamond, that keeps the line height at normal at every size, which is what the designer asked for by reserving that space.

We’ve been speaking in terms of ratios as is common in CSS. line-height takes a plain number, and f(s)f(s) only has one value that needs a unit: the font size, ss. So, to use the font size to make a ratio again, we divide by 1em in CSS:

.text {
--glyph: 0.948; /* (CapHeight - Descender) / UnitsPerEm */
--leading: 0.357; /* (Ascender - CapHeight + LineGap) / UnitsPerEm */
--fs-min: 1rem; /* Detail */
line-height: calc(var(--glyph) + var(--leading) * var(--fs-min) / 1em);
}

This is where units finally come in. The space between lines is set by the Detail size, so it needs a font size to compare against. var(--fs-min) / 1em is the fraction smin⁡s\frac{s_{\min}}{s}. Both are font sizes, so dividing one by the other gives a plain number, and the line height stays a ratio.

Dividing one length by another directly like this needs CSS typed arithmetic.

Warning

Browsers without typed arithmetic ignore the whole declaration. To support them, multiply the formula through by 1em so nothing divides by it. The line height becomes a length instead of a ratio:

line-height: calc(var(--glyph) * 1em + var(--leading) * var(--fs-min));

Avoid Jane Ori’s tan(atan2()) trick here. Safari reads the angle from atan2() in the wrong unit when one of its lengths is in em, which can bring the line height down to 0 and stack every line on top of the last.

Warning

The browser works out 1em on the element that declares line-height, and children inherit the result, not the formula. An element that changes its font-size without declaring line-height keeps a line height worked out for its parent’s size.

Always declare font-size and line-height together:

h1 {
font-size: 1.5rem;
line-height: calc(
var(--glyph) + var(--leading)
* var(--fs-min) / 1em
);
}

This formula doesn’t need to use clamp(). As the type size changes, the ratio keeps adjusting toward the glyph height without reaching it. A fluid Heading can grow however large it might, and the space between its lines stays the same size.

If you want more room between every line, raise the line gap. It’s the breathing room the font’s designer set aside, and since it’s part of the visual leading, the formula keeps it the same size in pixels at every font size. Many fonts, EB Garamond included, ship with a line gap of 00.

To choose a line gap, start from the line height you want for Body text, such as the 1.5 the USWDS asks for on longer text. Take away the glyph height, multiply by the Body size divided by the Detail size, and convert back to font units. Then take away the part of the visual leading the font already has:

LineGap=(1.5−glyph)×sbodysmin⁡×UnitsPerEm−(Ascender−CapHeight)\begin{aligned} &\text{LineGap} = (1.5 - \text{glyph}) \times \frac{s_{\text{body}}}{s_{\min}} \times \text{UnitsPerEm} \\ &\quad - (\text{Ascender} - \text{CapHeight}) \end{aligned}

The experimental section below lets the length of the line raise the visual leading instead.

While a web font loads, the browser shows a fallback font in its place. The fallback has its own metrics. Since this chapter sets the line height directly, each line still takes up the same space. But the letters can sit higher or lower within the line, and wider or narrower letters change where lines wrap. When the web font arrives, the text jumps.

You can reshape a local fallback to match your web font with @font-face. The override values are the same metrics we used above, as percentages of the font size:

@font-face {
font-family: "EB Garamond Fallback";
src: local("Times New Roman");
size-adjust: 100%;
ascent-override: 100.7%; /* Ascender / UnitsPerEm */
descent-override: 29.8%; /* -Descender / UnitsPerEm */
line-gap-override: 0%; /* LineGap / UnitsPerEm */
}
body {
font-family: "EB Garamond", "EB Garamond Fallback", serif;
}

The size-adjust descriptor scales the fallback’s letters so they’re about as wide as the web font’s. You should know that size-adjust also scales the overrides. Once you set it to anything other than 100%, divide each override by it. As an example:

ascent-override=AscenderUnitsPerEm×size-adjust\text{ascent-override} = \frac{\text{Ascender}}{\text{UnitsPerEm} \times \text{size-adjust}}

Not every browser supports these overrides yet. Where they’re missing, the fallback shows as it would have without them. To match the size of lowercase letters across typefaces as well, see font-size-adjust in the Font size chapter.

That covers font metrics and size. Next, we’ll look at how line height relates to line length.

The following section is not recommended. Changing the line height on otherwise similar content can look inconsistent and distract readers. We’ll still walk through the steps, because some of the techniques are new and could be useful elsewhere.

The next exploration is letting the length of the line, or measure, affect the line height. Line length changes how far the eye travels back to find the start of the next line. Longer lines need more space between them to make that easy. Short lines, like a Heading’s, can sit tighter.

The formula from above already takes care of font size. It keeps the space between lines the same as the type grows. Line length should only change that space, the visual leading. The glyph height belongs to the letters, so it stays where the font’s metrics put it. Longer lines need more space between them.

Starting from y=mx+by = mx + b and rearranging the terms gives the visual leading for any line length, ww:

ℓ(w)=(w−wmin⁡wmax⁡−wmin⁡)×(ℓmax⁡−ℓmin⁡)+ℓmin⁡\begin{aligned} \ell(w) = {} &\left( \frac{w - w_{\min}}{w_{\max} - w_{\min}} \right) \\ &\times (\ell_{\max} - \ell_{\min}) + \ell_{\min} \end{aligned}
Note

To see how this comes from y=mx+by = mx + b, follow the working in the Headings chapter, which uses a similar formula.

This takes the place of the single visual leading in the formula from above. The glyph height and the font size work the same as before:

f(s,w)=glyph+ℓ(w)×smin⁡sf(s, w) = \text{glyph} + \ell(w) \times \frac{s_{\min}}{s}

For ℓmin⁡\ell_{\min}, use the font’s own visual leading, ℓ\ell, from earlier. Short lines don’t need any more space than the designer set aside. For EB Garamond:

ℓmin⁡=ℓ=0.357\ell_{\min} = \ell = 0.357

To choose ℓmax⁡\ell_{\max}, start from the line height you want on a full line of Body text. The USWDS asks for at least 1.5 on longer text. Take away the glyph height, then multiply by the Body size divided by the Detail size. With Body text at 19.2px:

ℓmax⁡=(1.5−glyph)×sbodysmin⁡=(1.5−0.948)×19.216=0.66\begin{aligned} &\ell_{\max} = (1.5 - \text{glyph}) \times \frac{s_{\text{body}}}{s_{\min}} \\ &\quad = (1.5 - 0.948) \times \frac{19.2}{16} \\ &\quad = 0.66 \end{aligned}

Here’s how the line height of Body text changes with the length of its lines. It goes from about 1.2461.246 on a 320px line to 1.51.5 on a full 60rch line (using wmin⁡w_{\min} and wmax⁡w_{\max} from the Headings chapter):

w min = 320, f(w) = 1.2455w max = 460.8, f(w) = 1.498020040060080011.52measure (px)line-height
glyph=0.948\text{glyph} = 0.948, ℓmin⁡=0.357\ell_{\min} = 0.357, ℓmax⁡=0.66\ell_{\max} = 0.66, smin⁡=16s_{\min} = 16, s=19.2s = 19.2, wmin⁡=320w_{\min} = 320, wmax⁡=460.8w_{\max} = 460.8

In CSS, progress() moves the visual leading between the two amounts based on line length. It never returns less than 0 or more than 1, so the leading stays between them without a clamp(). The line height is the formula from above, with --leading now set by the line length:

.text {
--glyph: 0.948; /* (CapHeight - Descender) / UnitsPerEm */
--leading-min: 0.357; /* (Ascender - CapHeight + LineGap) / UnitsPerEm */
--leading-max: 0.66; /* Body at 1.5 on a full line */
--fs-min: 1rem; /* Detail */
--w-min: 320px;
--w-max: 60rch;
--leading: calc(
progress(var(--w), var(--w-min), var(--w-max))
* (var(--leading-max) - var(--leading-min))
+ var(--leading-min)
);
line-height: calc(var(--glyph) + var(--leading) * var(--fs-min) / 1em);
}

Because the font’s metrics only appear in --glyph and --leading-min, each font family can have its own. If your Headings use a different font, give them that font’s values and keep the same --leading-max. The space between lines on a full line is then the same in both fonts.

So how do we get the width (--w) of any passage of text?

In JavaScript, getting an element’s width is easy: element.clientWidth. The tricky part is that we’d need it to update live. In responsive design, widths change often. You might reach for new ResizeObserver() to react to each resize, but attaching one to every block of text just to adjust line height wastes performance.

You might try a container query next, since we’re asking about the width of the text. But we’d need a separate query for every possible width, one per pixel, which isn’t realistic. And container query units only give us a percentage of the container’s size, not the width of the text itself.

Amazingly, there’s a way to measure an element using only CSS, with scroll-driven animations.

@property --progress {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@property --w {
syntax: "<length>";
inherits: true;
initial-value: 0px;
}
@keyframes x { to { --progress: 1} }
.text {
--w: calc(1px / (1 - var(--progress)));
display: inline-block;
align-self: start;
overflow: auto;
timeline-scope: --cx;
animation: x linear;
animation-timeline: --cx;
animation-range: entry 100% exit 100%;
&::before {
content: '';
width: 1px;
display: block;
view-timeline: --cx inline;
}
}

The linked article explains how this works. A few notes for our use:

  • The element is inline-block, so it’s only as wide as its content. That gives us an accurate width. A block element would always fill its container.
  • align-self: start; stops a parent layout, like flex or grid, from stretching the element to fill the container.
  • This approach avoids the position property, so it doesn’t create unneeded stacking contexts.
  • --w is registered as a <length> and calculated from 1px, so it’s already a width in pixels. It can go straight into progress() alongside --w-min and --w-max.

In print, designers often used a grid to align elements and text. On the web, this is mostly unnecessary, since browsers have their own ways of aligning things. But if you want your line height to line up with a design grid, you can use the CSS round() function.

Say you have an 8px grid, where everything is sized in multiples of 8. With a line height of 1.5, a 1rem (ie., 16px) font sits in a line about 24px tall. That works, since 24 is a multiple of 8. The trouble is the other sizes. On a modular scale, the next size probably won’t give a multiple of 8. We need the line height to stay a multiple of 8 as the font size changes.

Here’s where round() comes in:

.text {
line-height: round(up, 1.5em, 8px);
}

As the font size changes, so does the em in 1.5em. We need em here because the rounding step (8px) has units of its own. The 1.5 can come from the calculations above, if you like.

The catch is that, because the line height snaps to the 8px grid, the space between lines will vary between font sizes. A size that lands just under a grid line gets less space between lines than one that doesn’t.

It also lines up the line boxes, the space that represents 1lh, not the letters. The space that rounding adds becomes CSS leading, split evenly above and below each line as half-leading, so the text sits in the middle of its box. Each size gains a different amount, so its baselines land at a different distance from the grid lines. In print, a grid usually lines up the baselines, which round() alone can’t do. You’d need to do more work to get the letters to set their baseline on the grid from here.

So ask yourself what matters more: lining text up with the grid, or a reading experience that’s consistently easy?