Font-Display Values Explained: Configuration & Performance Workflow
This guide is part of the Font Loading & Delivery Strategies blueprint, and it covers the single CSS descriptor that decides what a visitor sees while a web font is still on the wire: font-display. That descriptor controls the browser's font loading timeline directly, dictating the trade-off between Cumulative Layout Shift (CLS) and Largest Contentful Paint (LCP). Misconfigure it and you ship invisible text (FOIT) or a jarring late swap (FOUT) on the very visit that matters — the first, uncached one. This diagnostic workflow targets frontend engineers optimizing render-blocking typography, and it prioritizes deterministic fallback chains and measurable render-path mitigation over guesswork.
Diagnosing a font-display Problem
Start by reproducing the failure rather than theorizing about it. Open Chrome DevTools, go to the Network panel, set throttling to Slow 4G, check Disable cache, and reload. Watch the page paint: if text is invisible for a beat and then appears, you have a block period (FOIT) longer than zero. If text appears immediately in a system font and then visibly reflows when the web font arrives, you have a swap (FOUT) — which is correct behavior, but the reflow needs metric matching. If the web font sometimes never appears at all on the slow profile, you are looking at optional skipping the swap. Confirm the descriptor that produced this: in the Elements panel inspect the relevant @font-face rule, or run document.fonts in the console and read each FontFace.display value. A face with no explicit descriptor resolves to auto, which most browsers treat like block — that is the most common root cause of an unexplained FOIT.
The metric to put a number against is the block period duration. Wrap a PerformanceObserver on layout-shift entries (for the FOUT reflow) and read PerformanceResourceTiming for the font's responseEnd to know when the file actually arrived. The gap between paint and that timestamp, multiplied by your CLS contribution, is the budget you are trying to cut.
How the Block and Swap Periods Actually Work
The CSS Fonts specification splits every font load into two consecutive windows. The block period is the time the browser renders text invisibly while waiting for the font — characters take up layout space but paint nothing, which is the technical definition of FOIT. The swap period follows: the browser renders a fallback font and then swaps the web font in the moment it arrives, which is FOUT. Each font-display value is simply a different choice of how long those two windows last.
Trade-off Matrix:
block: 3s block period, infinite swap period. For up to three seconds the browser shows nothing, then shows the fallback and swaps the web font in whenever it lands. Causes extended invisible text on slow connections. Avoid except for icon fonts, where a fallback character is meaningless garbage and invisible is genuinely better.swap: 0ms block period, infinite swap period. Text is immediately visible in the fallback font; the web font swaps in whenever it is ready, no matter how late. Best for body text and long-form content where readability beats brand fidelity.fallback: 100ms block period, 3s swap period. A very short invisible flash (usually imperceptible), then the fallback. If the web font has not loaded within roughly 3.1s of the load starting, the browser keeps the fallback permanently for that page view and discards the late web font from rendering. Ideal for secondary UI where a late swap would be more disruptive than the fallback itself.optional: 100ms block period, 0s swap period. Same brief block, but a zero-length swap window — the web font only renders if it is already available (cached, or fetched within the ~100ms block). On a slow connection the browser may decline to load the font at all and keeps the fallback. This is the only value that guarantees zero layout shift from a late swap.auto: defers to the user agent, which in every shipping browser behaves likeblock. A bare@font-facewith no descriptor isauto, so it inheritsblock's 3s FOIT.
The key asymmetry: swap and block both have an infinite swap window, so the web font always eventually wins and you always pay a potential CLS reflow. fallback and optional bound or eliminate that window, trading brand fidelity for layout stability.
Baseline Configuration
The minimum correct setup is an explicit descriptor on every @font-face — never rely on the auto/block default. Pair body text with swap and a metric-matched fallback so the inevitable reflow is invisible.
Baseline @font-face with explicit swap descriptor
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-latin-400.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
This alone removes FOIT on body text. To stop the swap from shifting layout, declare a fallback @font-face with metric overrides so the system font occupies the same space the web font will. That fallback-metric technique is covered in depth in fallback font stack design; the descriptors live on the fallback face, not the web font.
Metric-matched fallback to flatten the swap reflow
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
/* Then: font-family: 'Inter', 'Inter Fallback', sans-serif; */
Step-by-Step Selection & Verification Workflow
Each step ends with a verification check so you ship a known-good descriptor rather than a guess.
Implementation Steps:
- Classify each typeface by role. Split your
@font-facerules into critical body text, headings, and decorative/icon faces. Verify: every face has a written role; nothing is unclassified. - Assign a descriptor per role. Body text gets
swap; secondary headings toleratefallback; purely decorative display fonts getoptional; icon fonts getblock. Verify: rundocument.fonts.forEach(f => console.log(f.family, f.display))and confirm no face reports the defaultauto. - Preload only the above-the-fold weights. Add
<link rel="preload" as="font" type="font/woff2" crossorigin>for the one or two weights your first paint uses, sourced via preloading & resource hints. Verify: DevTools Network shows those fonts at Highest priority with no "preloaded but not used" console warning. - Throttle and observe the timeline. Set Slow 4G, disable cache, reload. Verify: body text is readable at first paint (no FOIT) and the swap-in is imperceptible because the fallback is metric-matched.
- Measure CLS from the swap. Run a
layout-shiftPerformanceObserver across the load. Verify: the cumulative score stays under 0.1 even when the font swaps in late. - Check the optional/decorative faces on a slow profile. Verify:
optionalfaces correctly keep the fallback on Slow 4G (the web font may not appear at all — that is intended), whileswapfaces always eventually swap. - Audit for descriptor conflicts. Search the bundle for the same family declared with different
font-displayvalues across@importand local rules. Verify: eachfont-familyhas exactly one descriptor per weight/style.
Browser Compatibility & Fallback Matrix
| Capability | Chrome/Edge | Firefox | Safari |
|---|---|---|---|
font-display descriptor |
60+ | 58+ | 11.1+ |
optional skips on slow net |
Yes (network-aware) | Yes | Yes |
size-adjust (fallback metrics) |
92+ | 92+ | 17+ |
ascent/descent/line-gap-override |
87+ | 89+ | 17+ |
fetchpriority="high" on preload |
102+ | 119+ | 17.2+ |
| Variable-font weight ranges | 62+ | 62+ | 11+ |
Two edge cases matter. First, browsers without font-display support (anything pre-2018) ignore the descriptor and fall back to their native loading behavior — usually a 3s block — so the descriptor is a progressive enhancement that never hurts. Second, Safari only gained metric-override descriptors in version 17; on older Safari your swap reflow will not be flattened by size-adjust, so you must accept a slightly larger CLS contribution there or restrict the swap window with fallback.
Configuration Examples
Standard @font-face with swap and an optimized unicode-range subset
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2-variations');
font-weight: 100 900;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
A smaller file reaches the browser sooner, which shrinks the practical FOIT window under block/fallback and shortens the time before a swap settles. Split scripts with unicode-range subset loading so only the ranges your content uses are fetched.
Per-role descriptor assignment across a type system
/* Body: never hide text */
@font-face { font-family: 'Inter'; src: url('/fonts/inter-400.woff2') format('woff2'); font-display: swap; font-weight: 400; }
/* Secondary heading: brief block, bounded swap, keep fallback if very late */
@font-face { font-family: 'Inter'; src: url('/fonts/inter-700.woff2') format('woff2'); font-display: fallback; font-weight: 700; }
/* Decorative display: only render if it is essentially free */
@font-face { font-family: 'Lobster'; src: url('/fonts/lobster.woff2') format('woff2'); font-display: optional; }
/* Icon font: fallback glyphs are meaningless, so invisible is preferable */
@font-face { font-family: 'AppIcons'; src: url('/fonts/icons.woff2') format('woff2'); font-display: block; }
CSS Font Loading API hook to coordinate descriptor behavior
if ('fonts' in document) {
document.fonts.load('16px Inter').then(() => {
document.documentElement.classList.add('fonts-loaded');
});
}
For a promise-driven approach that takes the swap out of the browser's passive timeline entirely, see the CSS Font Loading API implementation.
Next.js font module with automatic descriptor injection
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
preload: true,
adjustFontFallback: true, // auto-generates the metric-matched fallback face
variable: '--font-inter',
});
Common Pitfalls
- Leaving the descriptor off entirely. A bare
@font-faceresolves toauto, which behaves likeblock— up to 3s of invisible text. Always setswap,fallback, oroptionalexplicitly. - Confusing
blockwithswap.blockhides text for up to 3 seconds;swapshows the fallback immediately with a 0ms block. Reaching forblockto "avoid the flash" is the most common self-inflicted FOIT. - Using
swapwithout a metric-matched fallback. The web font always swaps in, so withoutsize-adjust/ascent-overrideon the fallback you get a visible reflow and a CLS penalty. The descriptor fixes FOIT, not CLS — fallback metrics fix CLS. - Using
optionalon critical UI text. On slow connections the web font never loads, so those users always see the fallback. Reserveoptionalfor genuinely decorative type. - Conflicting
font-displayvalues across@importand local@font-facefor the same family. The browser's resolution is unpredictable; declare each weight/style once. - Omitting
unicode-range, so the swap waits on an oversized payload. A 200KB+ multi-script file delays the swap settle and lengthens any block window; subset to the ranges you actually render.
Frequently Asked Questions
Does font-display: swap negatively impact LCP?
Only when the LCP element's text is rendered in the web font and that font has not preloaded. With swap the fallback paints immediately, so first paint is not blocked — but LCP is measured against the final rendered element, so a late swap can push it. Pair swap with <link rel="preload"> and a subset payload under ~50KB so the font arrives before LCP settles.
Can font-display be applied to variable fonts?
Yes. The descriptor syntax is identical and lives on the same @font-face rule that declares the weight range (font-weight: 100 900). The block/swap timeline applies to the whole variable file; once it arrives, weight and axis interpolation happen instantly with no additional swap.
What is the recommended fallback for optional display values?
A system UI stack such as system-ui, -apple-system, sans-serif, chosen so its x-height and weight roughly match the web font. Because optional may keep that fallback permanently for a page view, pair the fallback @font-face with size-adjust and ascent-override so the kept fallback occupies the web font's intended space and produces no layout shift at all.
Is there ever a reason to keep using auto?
Rarely. auto hands the decision to the browser, which in practice means block everywhere today, but the spec permits the user agent to use network and user preferences to choose differently in future. If you care about deterministic rendering — and on a performance-critical page you should — set an explicit value rather than auto.