How to apply font-display strategies in build pipelines
- Step 1Fix the mapping you will automate — Use the picker once per surface priority to confirm the values, then write the mapping down as the spec: display/brand faces → `swap`, performance-critical body faces → `optional`, everything balanced → `swap`. Decide whether any body surface needs the manual `fallback` value (the picker does not emit it). This mapping is what your pipeline encodes.
- Step 2Model surfaces as design tokens — Add a `font-display` token per font role in your design-token source (e.g. `--font-display-display: swap; --font-display-body: optional;`). The token is the single source of truth; every `@font-face` references the token for its role rather than a hard-coded literal.
- Step 3Emit @font-face from the tokens — In your token build (Style Dictionary, a script, or a templated stylesheet), generate each `@font-face` with `font-display` set from the role token. For the per-face boilerplate (family name, sources, weight, style, `unicode-range`), the [font-face generator](/font-tools/font-face-generator) produces a block you can templatise — its `font-display` dropdown defaults to `swap`.
- Step 4Add a build-time lint that forbids auto/missing — Add a Stylelint rule or a small AST pass (PostCSS) that fails the build if any `@font-face` lacks `font-display` or sets it to `auto`. This converts silent drift into a red build. Allow only `swap`, `fallback`, and `optional` (plus `block` on an explicit icon-font allowlist).
- Step 5Normalise third-party CSS in the same pass — Google Fonts and other providers inject their own `font-display`. The [Google Fonts CSS generator](/font-tools/google-fonts-css-generator) lets you pick the value up front (`display=swap`), but for CSS you do not control, run a PostCSS pass that rewrites `font-display` on every `@font-face` to your token value so the whole bundle is consistent.
- Step 6Verify the built artifact, not the source — Minifiers can reorder or drop descriptors. After the build, grep the emitted CSS for `font-display` and assert every `@font-face` has exactly one allowed value. Then throttle-test the rendered page (DevTools Slow 4G) to confirm the runtime flash matches the strategy you encoded.
Picker mapping to encode in your pipeline
The exact value the browser picker emits for each use case. Replicate this in your token build or PostCSS pass. Note that balance maps to swap, and the picker never emits fallback, block, or auto.
| Surface role / use case | Picker output | Encode as token | Notes |
|---|---|---|---|
| Display / brand-critical | swap | --font-display-display: swap | Always show the brand font, accept the FOUT reflow |
| Body / performance-critical | optional | --font-display-body: optional | No mid-read reflow; web font may be skipped on first visit |
| General / balanced | swap | --font-display-default: swap | Safe default — web font is always eventually shown |
| Body, no late swap (manual) | (not emitted) | --font-display-body-strict: fallback | Set by hand; the picker has no use case that produces fallback |
| Icon font (manual allowlist) | (not emitted) | --font-display-icons: block | Only place block is defensible; keep it on an explicit allowlist |
Where to inject font-display in a pipeline
Comparison of the common automation points. The strategy picker is manual and browser-only; the pipeline must encode the rule itself.
| Approach | How it works | Best when |
|---|---|---|
| Design tokens → codegen | Token per role; @font-face generated with font-display from the token | You already own a token build (Style Dictionary etc.) |
| PostCSS / Lightning CSS pass | AST visitor sets/rewrites font-display on every @font-face | You have third-party or legacy CSS to normalise |
| Stylelint rule | Fails CI if font-display is missing or auto | You want to catch drift without changing source generation |
| Provider config | Request display=swap from Google Fonts up front | Fonts come from a hosted provider you can parameterise |
| Templated stylesheet | A .css template with the token interpolated per face | Small font set; no token tooling in place |
Cookbook
Pipeline snippets that encode the picker's mapping. The picker is browser-only; these are how you make the same decision automatic in CI. For per-face block boilerplate, the font-face generator produces a template you can interpolate.
Design tokens as the single source of truth
ExampleDefine font-display per role once. Every @font-face references the token, so changing the body strategy from optional to swap is a one-line edit in version control, not a hunt across stylesheets.
/* tokens.css — single source of truth */
:root {
--font-display-display: swap; /* brand-critical */
--font-display-body: optional; /* performance-critical */
--font-display-default: swap; /* balanced */
}
/* NOTE: font-display is a descriptor — it cannot read a var()
inside @font-face. Use the token to drive codegen below,
not as a live var() reference. */Codegen step that bakes the token into @font-face
ExampleBecause @font-face descriptors cannot use var(), resolve the token at build time and emit a literal value. This mirrors the picker's mapping in code.
// build-fonts.mjs
const MAP = { display: 'swap', body: 'optional', default: 'swap' };
const faces = [
{ family: 'Brand', file: 'brand.woff2', role: 'display' },
{ family: 'BodyUI', file: 'body.woff2', role: 'body' },
];
const css = faces.map(f => `@font-face {
font-family: "${f.family}";
src: url("/fonts/${f.file}") format("woff2");
font-display: ${MAP[f.role]};
}`).join('\n\n');
await fs.writeFile('dist/fonts.css', css);PostCSS pass that normalises every @font-face
ExampleFor third-party or legacy CSS you do not generate, rewrite font-display on every @font-face to your standard value so the whole bundle is consistent. Strip auto entirely.
// postcss-font-display.mjs
export default () => ({
postcssPlugin: 'font-display-normalize',
AtRule: {
'font-face'(rule) {
let decl = rule.nodes.find(n => n.prop === 'font-display');
if (!decl) rule.append({ prop: 'font-display', value: 'swap' });
else if (decl.value === 'auto') decl.value = 'swap';
},
},
});
// export const postcss = true;Stylelint rule that fails CI on drift
ExampleCatch the most common drift — a @font-face shipped without an explicit font-display, or set to auto — before it merges. declaration-property-value-allowed-list plus a custom check.
// stylelint.config.mjs (concept)
export default {
rules: {
// disallow auto on font-display anywhere
'declaration-property-value-disallowed-list': {
'font-display': ['auto'],
},
},
// plus a small plugin asserting every @font-face
// has a font-display descriptor at all.
};Standardise provider CSS at the source
ExampleGoogle Fonts injects its own font-display. Request the value you want up front so you do not have to rewrite it later. The Google Fonts CSS generator sets display=swap by default.
/* fonts.googleapis.com URL with explicit display */ https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap /* or generate a self-hosted block (display already applied) via /font-tools/google-fonts-css-generator, then run the PostCSS pass above to enforce consistency across the bundle */
Edge cases and what actually happens
Every row below was probed against the live API. Some documented requirements (alphabetical axis order, numerical tuple order) are not actually enforced in practice — useful to know if you've been blaming the wrong thing for a 400.
font-display cannot read a CSS var() inside @font-face
Spec limit@font-face descriptors do not support var() — font-display: var(--x); is invalid and the declaration is dropped, silently falling back to auto. So you cannot keep the token 'live' inside the rule; you must resolve it at build time and emit a literal value. The codegen and PostCSS cookbook entries do exactly that. This is the single most common mistake when people try to tokenise font-display.
The strategy picker has no CLI or API to call from CI
By designThe picker is a browser-side generative tool that formats an explainer and an @font-face skeleton; there is no command-line binary, no REST endpoint, and no build plugin shipped for it. Automate by encoding its mapping (branding→swap, performance→optional, balance→swap) in your own pipeline. Use the tool interactively to confirm the rule, then implement the rule in code.
Minifier reorders or drops font-display
Verify the artifactCSS minifiers occasionally normalise or, in pathological inputs (e.g. base64 with whitespace), drop an entire @font-face. Always assert against the built artifact, not the source: grep the emitted CSS for font-display and confirm every @font-face retains exactly one allowed value. Do not assume the source value survived minification.
Provider CSS overrides your token value
Normalise downstreamIf you import Google Fonts CSS at runtime, its @font-face rules carry the font-display the provider chose — which may not match your token. Either request the right value in the URL (display=swap) or run your PostCSS normalisation pass over the imported CSS at build time. A token map that only covers your own faces will not touch provider-injected ones.
auto sneaks in via copy-pasted legacy blocks
Lint to catchThe most common drift is a @font-face copied from old code that either omits font-display (defaults to auto) or sets it explicitly to auto/block. A Stylelint disallowed-list for font-display: auto plus a check that every @font-face has the descriptor at all turns this into a build failure instead of a slow production regression.
Per-surface values lost when faces share a family
Use distinct namesIf your display and body faces share one font-family name, you cannot give them different font-display strategies cleanly — the browser treats matching faces as alternatives. Emit distinct family names per role (Brand, BodyUI) in your codegen so each role can carry its own font-display token value. The mixed-surface pattern depends on separate names.
block on an icon font flagged by your own lint
Allowlist itIf your lint forbids everything except swap/fallback/optional, a legitimately-block icon font will fail CI. Maintain an explicit allowlist (by file path or family name) for icon faces that are permitted block. Do not relax the global rule — keep block an exception that must be named, not a default anyone can reach for.
Skeleton placeholders shipped to production
Reject in reviewThe picker's output uses font-family: "YourFont" and src: "/fonts/yourfont.woff2". If a developer pastes the skeleton verbatim into production, the family and path are wrong and the font never loads. Treat the skeleton as a spec for the font-display value only; generate the real block from your tokens or the font-face generator, and reject YourFont/yourfont.woff2 in code review.
Frequently asked questions
Can I call the font-display strategy picker from my build script?
No. The picker is a browser-side generative tool with no CLI, API token, or build plugin — it formats text in the page. Use it interactively to confirm the value per surface, then encode the same mapping (branding→swap, performance→optional, balance→swap) in your own pipeline via tokens, codegen, or a PostCSS pass.
Why can't I use a CSS variable for font-display inside @font-face?
@font-face descriptors do not support var(). font-display: var(--x); is invalid and is dropped, silently reverting to auto. Resolve the token at build time and emit a literal value (swap/optional/fallback) into the generated @font-face. Keep the token as the source of truth for codegen, not as a live reference in the rule.
What value should my generator emit by default?
swap, matching the picker's Balanced (and Brand-critical) recommendation and the font-face generator default. Use optional for performance-critical body faces. Reserve fallback for body surfaces where a late swap is unwanted, and block only for icon fonts on an explicit allowlist. Never emit auto.
How do I stop @font-face rules drifting back to auto?
Add a Stylelint rule that disallows font-display: auto and a check that every @font-face has a font-display descriptor at all, then run it in CI so the build fails on drift. Optionally add a PostCSS pass that rewrites missing/auto values to your standard swap automatically.
How do I apply different font-display values per surface automatically?
Model each font role as a token (--font-display-display: swap, --font-display-body: optional), give each role a distinct font-family name, and have your codegen bake the role's token into each @font-face as a literal. That keeps the per-surface decision in one place and in version control.
Does the font-face generator carry a font-display value I can standardise on?
Yes. The font-face generator has a font-display dropdown (auto/block/swap/fallback/optional) defaulting to swap. Templatise its output and override the value from your role token so generated blocks always match your pipeline standard.
How do I handle Google Fonts' own font-display in CI?
Request the value up front: the Google Fonts CSS URL accepts &display=swap, and the Google Fonts CSS generator sets display=swap by default. For CSS you import but do not generate, run a PostCSS pass that rewrites font-display on every @font-face to your standard value so the whole bundle is consistent.
Should I verify font-display in the source or the built CSS?
The built artifact. Minifiers can normalise descriptors and, on malformed input, drop whole @font-face rules. After the build, grep the emitted CSS and assert every @font-face has exactly one allowed font-display value, then throttle-test the rendered page to confirm the runtime flash.
Can I lint for missing font-display in @font-face?
Yes — a small Stylelint plugin can walk each @font-face at-rule and report any that lack a font-display descriptor, and declaration-property-value-disallowed-list can ban auto. Together they fail CI on the two most common drift cases. Keep an allowlist for icon fonts that legitimately use block.
Is fallback ever the right pipeline default?
Rarely as a global default — the picker emits swap for Balanced because swap always eventually shows the web font. Use fallback deliberately for specific body surfaces where a late (post-3s) swap would be jarring. Encode it as its own token (--font-display-body-strict: fallback) so the choice is explicit, since the picker will not generate it for you.
Does the picker upload anything during a build?
It cannot be used in a build — it is a browser tool. When you do use it interactively, it uploads nothing: the result badge reads 0 bytes uploaded and all work is local string formatting. Your pipeline does the automation; the tool just documents and recommends the value.
What's the minimum viable automation if I have no token tooling?
A templated stylesheet plus a Stylelint guard. Keep one .css template per font role with the literal font-display value, concatenate them at build, and run a Stylelint rule that bans font-display: auto. That captures most of the benefit (no drift, deliberate values) without adopting Style Dictionary or a full token build.
Privacy first
Every JAD Font tool runs entirely in your browser using opentype.js and the wawoff2 WASM Brotli encoder. Your fonts never leave your device — verified by zero outbound network requests during processing.