font-display and the Font Loading Timeline

Overview
The precise problem solved by `font-display` is not “making fonts faster”; it is controlling what the user agent is allowed to paint while a downloadable font face is unresolved. Without an explicit policy, a page can show invisible text while waiting for a font, show fallback text and then replace it later, or use implementation-specific timing. Those behaviours are commonly described as flash of invisible text and flash of unstyled text, but the standardised mechanism is the CSS font display timeline: a block period, a swap period, and a failure period tied to a specific font face.
The important systems point is that text layout is possible before the preferred webfont has arrived, because the browser can match a fallback font from the same font-family list. The trade-off is that fallback and webfont metrics may differ. A late swap can move line boxes, change glyph advances, and alter the size of a text-containing element. `font-display` exists to make that trade-off explicit per `@font-face`, rather than leaving every font request to a user-agent default.
How it works
At style resolution time, the browser matches text to a `font-family` list and then to available font faces by descriptors such as weight, style, stretch, and `unicode-range`. If the selected face is defined by `@font-face` and its font data is not yet available, the user agent may start a font fetch. CSS font fetching is integrated with Fetch and HTTP caching; a reusable cached response can satisfy the face without network transfer, while an uncached face competes with other discovered resources according to the browser’s scheduler.
The font display timeline begins when the user agent first attempts to use the downloaded font face for rendered text. During the block period, text is laid out using a fallback font but painted with an invisible fallback face if the real font is not ready. During the swap period, the fallback text is visible, and the browser swaps to the webfont if it finishes loading. During the failure period, the face is treated as failed for that use, so the fallback remains even if the font data arrives later.
The CSS Font Loading API exposes part of this machinery to script. `document.fonts` is a `FontFaceSet`; individual faces can be represented by `FontFace` objects, loaded with `FontFace.load()`, and observed through `loading`, `loadingdone`, and `loadingerror` events. `document.fonts.ready` resolves when layout has completed using the available fonts. This API does not replace `font-display`; it is an observation and control surface around the same font face set and loading state.
What the standard says
The `font-display` descriptor is defined for `@font-face` in CSS Fonts Module Level 4. The descriptor’s defined values are `auto`, `block`, `swap`, `fallback`, and `optional`. The specification defines the font display timeline as three consecutive periods: font block period, font swap period, and font failure period. The descriptor chooses the relative duration of those periods; it is not inherited, and it applies to the font face in whose `@font-face` rule it appears.
For `auto`, the user agent determines the strategy. For `block`, the specification describes a short block period followed by an infinite swap period. For `swap`, it describes an extremely small block period followed by an infinite swap period. For `fallback`, it describes an extremely small block period, a short swap period, and then failure. For `optional`, it describes an extremely small block period and then failure, with no swap period. The specification deliberately leaves exact durations user-agent-defined; public documentation commonly describes the extremely small period as around 100 ms or less and the short period as around three seconds, but those are not portable author-controlled constants.
font-display: swap;The font block period, the font swap period and the font failure period run one after another. Which one the font file lands in decides whether the reader waits, reads twice, or never sees the face at all.
web fontMatching the fallback's metrics removes the reflow without giving up the face: size-adjust: 92.6% on the fallback @font-face closes the width gap, and ascent-override / descent-override close the line-box gap.
Period lengths are the spec’s recommendations — 3s for a short block period, 100ms or less for an extremely small one — and the spec says user agents may use different durations. Chromium and Firefox block for up to about 3s; Safari blocks indefinitely. The shift value uses the Layout Instability definition (impact fraction × distance fraction, against a 360×800 viewport with 420px of content below), but the paragraph geometry is this model’s, not a measurement. CLS sums shifts across a visit, so one swap is a floor.
CSS Font Loading Module Level 3 defines the programmatic font loading model, including the `FontFace` interface, `FontFaceSet`, `FontFaceSetLoadEvent`, and status values such as `unloaded`, `loading`, `loaded`, and `error`. HTML defines `<link rel="preload">`, and the Fetch integration for `as="font"` matters because a preload must match the later font request. For cross-origin font URLs, the preload normally needs `crossorigin`, because font fetches use CORS semantics; otherwise the preload and the CSS font request can become separate fetches.
Trade-offs
The measurable trade-off is between text visibility latency and layout stability. `font-display: block` can preserve the intended font before first visible text, but the text may be invisible during the block period, which can delay meaningful reading and can affect Largest Contentful Paint if the largest candidate is text rendered only after the font is available. The documented Web Vitals thresholds for Largest Contentful Paint are good at 2.5 seconds or less, needs improvement above 2.5 seconds through 4.0 seconds, and poor above 4.0 seconds. These thresholds are guidance, not CSS conformance rules.
`font-display: swap` maximises early visible text because the block period is extremely small, but it permits a late metric-changing replacement. That can contribute to Cumulative Layout Shift when glyph widths, ascent, descent, or line gap differ between fallback and webfont. The documented Web Vitals thresholds for Cumulative Layout Shift are good at 0.1 or less, needs improvement above 0.1 through 0.25, and poor above 0.25. CSS metric override descriptors such as `size-adjust`, `ascent-override`, `descent-override`, and `line-gap-override` can reduce this risk by making the fallback face closer to the eventual webfont metrics.
`font-display: optional` is the strongest stability choice: after the extremely small block period, the browser can keep the fallback for that page view. It can also avoid spending network priority on a font that arrives too late to be useful. The cost is visual inconsistency between fast and slow paths, because some users may see the webfont and others may see the fallback. `font-display: fallback` is the middle option: it gives the webfont a short opportunity to arrive and then prevents very late swaps.
Failure modes
A common failure mode is treating `font-display` as a property on normal selectors. It is an `@font-face` descriptor; placing it on `body` or a class rule has no effect. Another is defining only one weight or style but using several in CSS. The browser may synthesize bold or italic, or it may match another face, so the observed timeline is not the one expected. DevTools font panels and the CSS Font Loading API can reveal which face was actually used and whether it is still `loading`, `loaded`, or `error`.
Preload errors are also frequent. A preload with a different URL, missing `crossorigin`, different credentials mode, or mismatched `as` value may not satisfy the later CSS request, producing duplicate transfers or a warning that the preloaded resource was not used. The minimal correct preload for a font uses `rel="preload"`, `as="font"`, the correct MIME hint such as `type="font/woff2"`, and `crossorigin` when the eventual font request uses CORS mode. Resource Timing entries can confirm whether a font was fetched, reused from cache, or blocked from detailed timing by the absence of the `Timing-Allow-Origin` response header.
Another diagnostic trap is expecting `optional` to swap after the font appears in the network panel. Under the specified timeline, `optional` has no swap period, so retaining fallback is valid. Conversely, visible shifting under `swap` is not a loading failure; it is the expected consequence of an infinite swap period combined with different metrics. Diagnosis should separate transport failure, font matching failure, timeline policy, and metric mismatch, because the same visual symptom can be caused by any one of them.
A minimal implementation
A correct minimal implementation defines the face once, chooses an explicit display policy, and supplies a fallback stack with compatible metrics. WOFF2 is the current compact webfont format to prefer when available. A first-viewport font may be preloaded, but preloading every face is counterproductive because each preload enters the resource scheduler early. The minimal rule is to preload only the exact face needed for initial rendering, using the same URL that appears in `src`.
Example:
```html
<link rel="preload" href="/fonts/ui-regular.woff2" as="font" type="font/woff2" crossorigin>
<style>
@font-face {
font-family: "UI Text";
src: url("/fonts/ui-regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
:root {
font-family: "UI Text", system-ui, sans-serif;
}
</style>
```
This is minimal because the policy is attached to the font face, the fallback is usable immediately, and the preload, if retained, matches the later font request.
If layout stability is more important than typographic replacement, change the descriptor to `font-display: fallback` or `font-display: optional`. If the chosen policy allows swapping, measure the result rather than assuming safety: inspect font request timing, observe `document.fonts.ready` for completion, and check layout-shift attribution in performance tooling that exposes the Layout Instability API. The implementation is correct when text is never unintentionally invisible, duplicate font fetches are absent, and any remaining shifts are explained by known metric differences rather than by accidental loading behaviour.