slop audit
Blog post 52 / Performance

font-display: choose swap, fallback, or optional

Choose font-display settings for readable text, compare swap, fallback, and optional, and test slow or failed font loads before launching your website.

By Slop Audit · 8 min read

font-display controls what visitors see while a web font loads: briefly invisible text, a fallback that can change later, or a fallback that stays for that page load. Choose the value around the page's reading and interaction needs, then test slow and failed font requests on the deployed URL. Adding swap everywhere does not prove that text appears quickly or stays in place.

This guide focuses on the font-loading decision. Use the Largest Contentful Paint debugging guide to investigate delayed main content, and the Cumulative Layout Shift guide to trace movement from fonts and other causes.

font-display launch checklist

  1. Identify the font faces used by headings, body text, navigation, and buttons.
  2. Choose whether a late change of typeface is acceptable for each face.
  3. Add the descriptor inside the relevant @font-face declaration.
  4. Check the fallback at narrow widths, with long labels and real content.
  5. Test normal, slow, failed, and returning-browser loads.
  6. Confirm which font actually renders, rather than trusting the CSS family list.
  7. Save the conditions and screenshots alongside the release's performance evidence.

Compare font-display swap, fallback, and optional

The MDN font-display reference describes five values. The useful distinction is how long text can remain invisible and whether a downloaded face may replace the fallback afterward:

  • auto: the browser chooses the display strategy.
  • block: a short blocking period followed by an unlimited opportunity to swap.
  • swap: an extremely small blocking period followed by an unlimited opportunity to swap.
  • fallback: an extremely small blocking period and a short opportunity to swap.
  • optional: an extremely small blocking period with no subsequent swap period.

These are browser-controlled periods. Avoid building a test around an assumed exact number of milliseconds. The CSS Fonts specification defines the loading timeline and explains that its timer starts when the browser first attempts to use the face.

Use swap when you want text to become readable promptly and the preferred face to appear even if it arrives late. Accept that the change may be noticeable. Consider fallback when a brief opportunity to replace the fallback is acceptable but a much later change is distracting. Consider optional when keeping the fallback for the current view is an acceptable design outcome.

For example, a student portfolio's article text may work well with optional if its system-font version is equally readable. A heading whose typeface is central to the design might use swap, with deliberate work to make fallback wrapping similar. These are starting hypotheses to test, not universal presets.

Put the descriptor in @font-face

For a self-hosted regular font, start with an explicit declaration and a usable fallback stack:

@font-face {
  font-family: "Launch Sans";
  src: url("/fonts/launch-sans-regular.woff2") format("woff2");
  font-style: normal;
  font-weight: 400;
  font-display: swap;
}

body {
  font-family: "Launch Sans", system-ui, sans-serif;
  line-height: 1.5;
}

Replace the example family and file with a font you are licensed to serve. This declaration covers only the regular face; declare the actual bold or italic faces separately when the design uses them. Keep the descriptors aligned with the files rather than labeling a regular file as several unrelated weights.

Putting font-display: swap in body does not configure the downloaded face. If a framework or font provider generates the declarations, inspect the generated production CSS and change that integration's configuration. A second unused declaration in a different stylesheet will not repair the active one.

First verify that the file is delivered successfully. A display strategy cannot fix a wrong URL, an HTML fallback response, a font blocked by CSP, or a failed cross-origin request. Use the browser console troubleshooting workflow when the font request fails before you evaluate its visual behavior.

Keep the fallback layout usable

A fallback is part of the interface. Review the page while that font is actually in use:

  • Does the hero heading wrap into an extra line and cover the primary action?
  • Do navigation labels still fit without clipping?
  • Can a translated button label grow without overlapping its neighbor?
  • Are prices, code samples, and error messages readable?
  • Does the layout remain usable when the preferred font never arrives?

Google's font best-practices guide explains the tradeoff between quickly showing text and avoiding layout shifts from swapping. Similar fallback metrics can reduce movement. Compare the actual text at the widths where its line breaks change; a generic font-family match is not enough.

If you use metric overrides such as size-adjust on a fallback face, derive the values from your chosen fonts and verify them across the supported platforms. Do not copy percentages from an unrelated font pairing. Start by fixing rigid component widths and heights that make a small text change break the layout.

Avoid icon fonts for essential action labels. In the font-failure test, an unlabeled private-use glyph may become a missing character. Give the control a persistent text label or use an appropriately labeled SVG so the task does not depend on a successful font download.

Preload only a measured critical face

A preload can make a needed font discoverable earlier, but it is a separate decision from how text displays. If a trace shows that this exact face is critical to the first view, a matching hint can look like this:

<link
  rel="preload"
  href="/fonts/launch-sans-regular.woff2"
  as="font"
  type="font/woff2"
  crossorigin
>

The MDN preload documentation explains why font preloads need a matching CORS mode, including for same-origin fonts. Keep the URL identical to the one the active CSS requests. Verify request reuse in the Network panel instead of assuming the hint works.

Do not preload every weight, language subset, or below-the-fold face. Write down which first-screen text needs the file, compare a fresh-load trace before and after the hint, and remove it if it does not improve the intended result. Keep total font transfer within your website performance budget.

Test font-display on slow and failed loads

Use a fresh browser profile or disable the cache for the initial-load pass. Then repeat with a warm cache as a separate case. A returning visit can hide the delay you need to reproduce.

For each critical route, use this test matrix:

  • Normal delivery: check that the intended faces are delivered and used.
  • Slow font delivery: record the first text paint and any later typeface change.
  • Blocked font delivery: confirm that content and controls remain understandable.
  • Warm reload: check request reuse and the resulting typography.

In DevTools, filter Network requests to fonts, inspect the requested file and its initiator, then review the rendered-font information for a heading and a paragraph. Chrome documents this under Rendered Fonts in Elements > Computed. The computed font-family list states the requested choices; it does not identify which face supplied each glyph.

Do not use document.fonts.check() as proof that your branded font rendered. As the MDN check() reference explains, it can return true for a nonexistent font. Its result is about whether matching faces would need further loading, not a visual assertion about your chosen typeface.

For optional, a successfully downloaded file does not mean the page must swap to it after the display window has passed. Evaluate the visible result and the intended policy together. A deliberate fallback is different from a failed deployment.

Add a font-failure check to Playwright

A small failure test can protect the fallback path. This example assumes a public preview URL, a main heading, and an ordinary link to /contact. Replace those selectors with your page's contract:

import { test, expect } from "@playwright/test";

test.use({
  viewport: { width: 375, height: 812 },
  serviceWorkers: "block",
});

test("the main task survives failed web fonts", async ({ page }, testInfo) => {
  const target = process.env.PREVIEW_URL;
  if (!target) throw new Error("Set PREVIEW_URL to your deployed preview");

  let blockedFonts = 0;
  await page.route("**/*", async (route) => {
    if (route.request().resourceType() === "font") {
      blockedFonts += 1;
      await route.abort();
    } else {
      await route.continue();
    }
  });

  await page.goto(target, { waitUntil: "load" });
  await expect(page.locator("main h1")).toBeVisible();
  expect(blockedFonts).toBeGreaterThan(0);

  await testInfo.attach("font-failure-mobile", {
    body: await page.screenshot({ fullPage: true }),
    contentType: "image/png",
  });

  await page.getByRole("link", { name: "Contact us", exact: true }).click();
  await expect(page).toHaveURL(new URL("/contact", target).href);
});

Playwright documents request abortion and blocking service workers. The blocked-request count prevents a misleading pass when the test never intercepted a font. Inline font data or a page that intentionally uses only system fonts needs a different contract.

Review the attached screenshot: a visibility assertion alone does not prove that glyphs painted correctly or text remained legible throughout loading. This test verifies a failed-font task path; it does not measure swap timing or CLS. Keep a separate slow-load recording for those questions, then add the test to your website regression routine.

Check the public release with Slop Audit

After choosing and testing your font-display policy, run Slop Audit on the deployed URL. When Browser Run is available, the report provides desktop and mobile screenshots, bounded lab LCP and CLS evidence, and grouped same-site runtime or failed-request evidence. Use those observations to find a delayed heading, a moved control, or a delivery failure worth reproducing in your font tests.

Slop Audit does not inspect font-display values, verify which font rendered, throttle font requests, or run the blocked-font scenario above. A passing report cannot establish that every font-loading state works. Keep your font-specific evidence alongside the public audit, fix the affected CSS or delivery path, and rerun both after the next typography change.