- Use
remfor font sizes. - Use only 3 categories of text: Heading, Body, and Detail.
- Use modern CSS
pow()to create a modular scale. - Body and Detail text don’t need to be fluid.
- Consider letting a region’s density set the font size.
Use rem units for font size. One rem is the font size of the root element, html. It’s usually 16px, but not always. People can change their browser’s default text size, which makes 1rem larger or smaller.
You can also change the value of rem yourself by setting font-size on html:
html { /* Do not do this */ font-size: 62.5%;}This makes 1.6rem equal 16px. The trick was once popular because it made rem values easy for designers to convert to pixels. It’s no longer recommended. Most of the industry now expects 1rem to be about 16px, and third-party components built on that expectation may behave strangely if set otherwise. It’s also hard to add to or remove from an existing site, since every rem value changes with it.
It’s best not to set font-size on html at all. That way, the reader’s preferences come through, and 1rem means what other people expect it to mean.
Categories of text
Section titled “Categories of text”There are three categories of text, inspired by Adobe Spectrum: Heading, Body, and Detail. With only three, choosing what kind of text to use is a simple decision, separate from choosing its size. Each has a clear purpose.
Spectrum also has a fourth category, Code, for monospaced text. We won’t cover it here, since it’s less common for many consumer-facing applications.
Heading
Section titled “Heading”Heading text introduces a new idea. It’s usually a short phrase or sentence that leads the reader into the Body content that follows. I’d also give the Heading style to elements you might not think of as headings, such as <label> and <summary> since they also introduce new information but, that is up to your own discretion.
Headings are usually styled to stand out, so readers can find what interests them more quickly. They often use a different typeface from the rest of the page. That’s fine, because there’s little Heading text to read, and its larger size usually keeps it readable.
Readers use Headings to decide whether the content that follows is relevant to them. That makes the Heading the highest priority text on the page, and usually the first thing a reader notices.
Heading sizes get their own chapter, since there are improvements we can make for Headings that matter less for other kinds of text.
Body text carries most of the information the reader came for. It allows longer phrases, paragraphs, and other formatting that help readers understand the topic. Body text must be easy to read, even more than Heading text, because this is where readers focus most closely. Inside applications, Body isn’t only text. Form fields belong here too, since filling them in is often how people complete a task. The <label> introduces the field, and the <input> is where the work happens.
Detail
Section titled “Detail”Detail text holds extra information. It isn’t critical, but it can help a reader who wants more than the Heading and Body give them. Examples include legal copy, help and error messages, and overline text (a small label above a heading, sometimes called an eyebrow).
Detail text gets the most scrutiny for accessibility. It’s often set too small, or in colors without enough contrast. Detail text is the lowest priority, but if it’s on the page, it matters, so it must be accessible.
These categories can appear in any order. For example, Detail text can sit above a Heading to show an article’s date or category.
The modular scale
Section titled “The modular scale”A modular scale is a set of font sizes designed to work well together, made popular by sites like utopia.fyi. You choose a base size and a ratio, which is how much each step grows. Every size comes from those two values, so changing either one updates the whole scale.
Each step multiplies the base by the ratio one more time. That’s the same as raising the ratio to a power:
For example, two steps up from 1rem with a ratio of 1.2:
You can use the interactive graph below to try out different values and see how they might affect the Heading, Body, and Detail text sizes.
Building it with pow()
Section titled “Building it with pow()”To build the scale in CSS, use the pow() function inside calc(). The pow() function allows you to raise a number to a power:
:root { --🚫fs-ratio: 1.2; --🚫fs-base: 1rem; --fs-0: calc(var(--🚫fs-base) * pow(var(--🚫fs-ratio), 0)); /* 16px */ --fs-1: calc(var(--🚫fs-base) * pow(var(--🚫fs-ratio), 1)); /* 19.2px */ --fs-2: calc(var(--🚫fs-base) * pow(var(--🚫fs-ratio), 2)); /* 23.04px */}The 🚫 in these variable names is unusual but valid. I use it to mark variables as private. Obvious at a glance, and it’s harder to type by hand. The later examples leave it out, so you can decide which variables to keep private.
Above, each name matches the exponent used to create that step. An exponent of 0 makes the ratio 1, so the first size equals the base. You could call it base instead. I prefer numbers, because developers are used to counting from zero, and it keeps the available steps predictable.
Anchor at the minimum
Section titled “Anchor at the minimum”You can use negative exponents to make sizes smaller than the base, and many type scale tools show them. I recommend that you don’t allow sizes below the base.
In the scale above, an exponent of -2 gives about 0.69rem (nice), or 11.1px. That’s too small for many people to read comfortably, and it’s only two steps below the base. Allowing sizes below the base opens the door to text that’s too small to read.
If you state that the base is the smallest size, no other font size can be smaller. Every other size grows from it. This means you only need to make sure the base is accessible, and every other size will be too.
There is a difference between the base font size and the default font size. In how this is thought, the base is the smallest accessible font size (ie., Detail). The default is the most commonly used font size. (ie., Body). It is thought as the default from the perspective of choice, as in: “If I was going to choose a font size, what category would I most likely be choosing most often?” This should be your default.
Sizing Body and Detail
Section titled “Sizing Body and Detail”Body and Detail sizes should be static. Only Headings need to be fluid. Fluid sizing is for text that would otherwise be too large for the screen it’s on. Body and Detail text are meant to wrap across many lines at a comfortable size, so they don’t need it.
Importantly, in this system, the base size is not set on <body>. The base is the smallest accessible size, and Detail text is meant to be less prominent than Body text. So Detail text gets the base, --fs-0, and Body text gets a larger step. This makes sure Body text is accessible too.
You could use a different scale for each kind of text. But then the sizes no longer work together, and you have more values to curate.
Matching typefaces
Section titled “Matching typefaces”Two typefaces at the same font size can look like different sizes. That’s because the font size doesn’t measure the letters. It measures an invisible box that every letter is drawn inside, called the em-square. One typeface might fill most of the box, while another leaves more empty space around its letters.
The measurements a font uses to place letters in that box are called its metrics. The one that matters most here is the x-height, the height of a lowercase letter like x. A larger x-height makes text look bigger, even at the same font size. So a Heading typeface with a small x-height can look a step smaller than the scale says, and a fallback font can look bigger or smaller than the web font it replaces.
Fonts don’t measure their metrics in pixels. Instead, the em-square is split into a set number of units, often 1000 or 2048. This number is the units per em. Divide any metric by it, and you get that metric as a relation to the font size.
The font-size-adjust property resizes the letters so the x-height is the same amount of the font size in every typeface. Set it to your main font’s x-height divided by its units per em:
FontDrop shows both, alongside the metrics in the Line height chapter.
:root { /* Replace 0.4 with your font's xHeight / UnitsPerEm */ font-size-adjust: ex-height 0.4;}This changes how large the letters are drawn, not the font size itself, so em and rem values stay the same.
Scoped font size
Section titled “Scoped font size”The right font size can also depend on where the text is placed. For example:
<div class="hero" data-density="cozy"> <div class="card" data-density="compact"></div></div>Here, data-density describes how roomy an area should feel. A cozy area should feel open, with larger text. A compact area should feel tighter, with smaller text. You could set the following in CSS:
[data-density="cozy"] { --fs-body: var(--fs-2); --fs-detail: var(--fs-1);}
[data-density="compact"] { --fs-body: var(--fs-1); --fs-detail: var(--fs-0);}Any text component that uses --fs-body gets a larger size in cozy areas and a smaller one in compact areas. The text component doesn’t need a size option, because the size comes from where it’s placed. This also keeps sizes consistent. Without a size option, there’s nothing to reason about. The text is simply the size its area gives it.