Fonts

Get your typeface on screen without a blank page first, and without the text jumping when it arrives.

07of 16
≈ 11Min read
5Parts

A font is a render-blocker in disguise

A web font is discovered inside a stylesheet, which itself blocks rendering — so the chain is HTML → CSS → font, three round trips before a single word appears. And by default the browser holds the text invisible for up to three seconds waiting for it.

That invisible period has a name, FOIT: flash of invisible text. The alternative, FOUT, shows a fallback first and swaps. Both are controlled by one property.

font-display, exactly

During the block period the text is invisible. During the swap period the font will still be used if it arrives. After both, the fallback is final for that page load.

optional is the only value that cannot cost you CLS, and it is the right default for body text if — and only if — the font can realistically arrive in 100 ms. That means subset, woff2, preloaded, and cached.

ValueBlock periodSwap periodResult
block≈ 3 sinfiniteinvisible, then your font whenever it lands — the default-ish behaviour
swap0infinitefallback immediately, swaps whenever it lands — guarantees a shift
fallback100 ms≈ 3 sa blink, then fallback; after 3 s your font is not used at all this visit
optional100 ms0if it is not ready in 100 ms, the fallback stays — no shift, ever

Make it arrive in time

crossorigin is not optional on the preload: fonts are fetched anonymously, and without it you download the file twice. unicode-range is how a subset stays correct — the browser only fetches the file if the page actually uses characters in that range.

Variable fonts are usually the bigger win: one file instead of four weights, often smaller than two of them.

the two lines that shorten the chain
<link rel="preload" href="/f/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
css
@font-face {
  font-family: Inter;
  src: url(/f/inter-latin.woff2) format("woff2");
  font-display: optional;
  unicode-range: U+0000-00FF, U+0131, U+2000-206F;   /* latin only — the subset */
  size-adjust: 107%;                                  /* match the fallback's x-height */
  ascent-override: 90%;
}

Killing the swap shift

Tools generate these numbers from the two font files. Done properly the swap is invisible and CLS is zero — which means you can safely use swap and always get your typeface.

a fallback tuned to match
@font-face {
  font-family: "Inter Fallback";
  src: local("Arial");
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22%;
}

body { font-family: Inter, "Inter Fallback", sans-serif; }

Or use none at all

font-family: system-ui, sans-serif costs zero bytes, zero round trips and zero shift, and on every platform it is the font the operating system spent years tuning for screens. For an app UI it is often the better choice, and the decision to load 200 KB of type should be made on purpose, not by a template.

Practice