
Build Note
next/font: why display optional beats swap
A late font re-wrapped the hero at 375px and cost 0.076 CLS. Keeping the size-adjusted fallback for the whole page view removes the swap instead of shrinking it.
Published August 5, 2026
Use display: "optional" rather than swap for self-hosted fonts. If the font misses its short block window, the size-adjusted fallback is kept for the whole page view instead of swapping in late and re-measuring the text — which is what turns a headline into a two-line headline and puts real shift into your field data.
The failure this fixes
A late-arriving web font changes the width of every glyph on the page. Where text wraps, that can add or remove a line, and everything below moves. Lab tests rarely catch it — by the time you profile, the font is cached and swaps instantly — so the symptom is a Cumulative Layout Shift number that looks fine locally and is not fine in the field.
The measurement on this site was specific: with swap, a late font re-wrapped the hero H1 at 375px viewports, adding a line and producing a 0.076 shift for slow-connection visitors. That is most of the way to the 0.1 threshold on a single element, on the page that matters most, for exactly the users already having the worst time.
| swap | optional | |
|---|---|---|
| Block period | Very short | Very short |
| If the font is late | Swaps in whenever it arrives | Fallback kept for this page view |
| Layout shift risk | Real, and worst on slow connections | None after first paint |
| First visit, slow network | Brand font, with a reflow | Fallback, no reflow |
| Repeat visits | Brand font | Brand font (cached) |
| What you're optimising | Brand fidelity on every visit | Stability on every visit |
The configuration
Self-host through next/font so the files come from your own origin — that also means no third-party font request to allow through a strict Content-Security-Policy. Set display and preload explicitly rather than relying on defaults.
/* display: "optional" (not "swap") is what keeps field CLS at 0: if a font
misses the ~100ms block window, the size-adjusted fallback is kept for the
whole page view — text is never re-measured mid-visit. With swap, late
fonts re-wrapped the hero H1 at 375px viewports (+1 line, a 0.076 shift
for slow-connection users). Preload + self-hosting means fast and repeat
visits still always render the brand fonts. */
const interTight = Inter_Tight({
subsets: ["latin"],
variable: "--font-inter-tight",
display: "optional",
preload: true,
})Three families are declared this way and exposed as CSS variables on <html>, so Tailwind can reference them without any runtime font loading logic. Nothing about this costs JavaScript: next/font generates the @font-face rules and the preload links at build time.
Why not just tune the fallback?
Because metric overrides get you close, and close still re-measures. Adjusting the fallback's size and spacing so it occupies almost the same space is genuinely useful — it is what makes optional tolerable to look at — but as a fix for shift it only shrinks the number. On a large headline at a narrow viewport, a small per-glyph difference is enough to change where the line breaks, and one extra line is not a small shift.
The stronger move is to remove the swap event rather than minimise its consequences. That is the difference between a metric that is usually under the threshold and one that is structurally zero, which is the same distinction behind sizing an animated headline from its longest state.
What else has to hold for CLS to stay at zero
Fonts are one of four usual causes and the least visible. The others are media without dimensions, content injected above existing content after paint, and anything whose height is decided by data that arrives late. Every image needs explicit width and height; every embed, banner, and async block needs its space reserved before it renders.
Handled together, zero is a normal result rather than an exceptional one — and it is the one Core Web Vital you can genuinely make structural rather than merely good. The other two take different work entirely.
Font strategy is decided once, early, and quietly determines a metric you will otherwise chase for months — one of several defaults set at the start of a build.
Questions
Won't some visitors never see the brand font?
Some first-time visitors on slow connections, yes — they get the size-adjusted fallback for that page view, and the real font on the next one once it's cached. That's the trade: a small number of visits render in a metrically matched substitute instead of every slow visit getting a reflow.
Why not use font-display: swap with a size-adjusted fallback?
That's the usual advice and it does shrink the shift rather than remove it. Metric overrides get the fallback close, not identical, so a late swap still re-measures the text. On a headline at a narrow viewport, close is enough to add a line. Optional removes the swap entirely for the visits at risk.
Does this apply to icon fonts?
Don't use one. An icon font is a render-blocking dependency whose failure mode is boxes or invisible glyphs, and inline SVG costs nothing at request time and can't shift. If you've inherited one, the same reasoning applies with more urgency.
How do I verify it worked?
Lab CLS is the wrong instrument here — it usually shows 0 either way, because the font is cached by the time you test. Throttle to a slow connection with an empty cache and watch the headline, then confirm on field data, which is where the shift was showing up in the first place.
Related notes
- Animating hero text without layout shift
- How to fix failing Core Web Vitals
- Keeping First Load JS under budget in Next.js
More on web development
- Claude Code rules for Elementor sites
- Claude Code rules for WCAG testing
- Cursor rules for WCAG testing
- Never tween opacity on animated text
Related service: Web Development
Want this kind of engineering on your project?
Tall Karol takes on fractional and project-based engagements for startups and agencies.
Book a working session