ScreenshotNeo

BlogHow-to

Chromatic Test Fails Because Fonts Load Too Late

Late web fonts can change text metrics after Chromatic captures a story. Find the failing font, preload it in Storybook, and wait for it deterministically.

By the ScreenshotNeo team4 October 202610 min read

A Chromatic snapshot or interaction can run before a custom web font has loaded. The browser initially renders fallback typography; when the intended font arrives, different glyph widths and line heights can reflow text and move nearby elements. The reliable fix is to make the exact font available to Storybook before capture: preload it in .storybook/preview-head.html, preferably from a local static asset. If a story must explicitly synchronize with font loading, await the browser Font Loading API.

Chromatic says it waits for resources such as fonts before snapshot capture, but external resources can still arrive late or fail, and asynchronous rendering makes later resources harder to detect. Diagnose the requested font URL first, then choose the smallest fix that preserves the typography your test is meant to verify. See Chromatic’s font loading, resource loading, and unstable test debugging guidance.

Why late fonts make Chromatic tests fail

Browsers commonly fetch a font when rendered content first needs it. Text may therefore be laid out with a fallback face, then laid out again with the custom face. The change can affect line wrapping, component dimensions, computed positions, and the result of interactions that measure or target text.

This can appear as a visual difference, a flaky snapshot, or an interaction that behaves differently across runs. A font might load before, during, or after component measurements or a play function. A fixed network delay makes the timing variable; the goal is to ensure the required font is ready before the work that depends on it.

Chromatic documents a 15-second story render window and an additional 15 seconds for interaction tests, and says it retries when resources do not arrive in time. Those limits do not make an unavailable or inconsistently hosted font deterministic. See Chromatic resource loading and interaction tests.

Find which font is late

  1. Open the failed build’s resource warning or unstable-test trace and identify the requested font URL.
  2. Run Storybook and inspect the browser’s network panel for that URL. Check the status, redirects, response content type, and whether the request is blocked or stalled.
  3. Confirm the URL in the computed CSS is the same file you intend to preload. A preload for a different weight, style, format, or URL does not satisfy the request.
  4. Check that the test browser can reach the asset without relying on a local-only path, authentication state, or network access unavailable to the build.
  5. Reproduce the story with a cold cache if the failure is intermittent. A warm local cache can hide a dependency on a slow external host.

Chromatic’s resource warning and trace are useful starting points; its unstable tests guide describes debugging traces. Also verify your own hosting and network rules rather than assuming every external asset is reachable in the test environment.

Fix 1: Preload the exact font in Storybook

Add a preload in .storybook/preview-head.html. Replace the example path, type, and family with the exact asset and face used by your CSS.

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

Use the same URL that the font-face rule requests. The crossorigin attribute is appropriate for font preloads, including same-origin font requests, and is required for cross-origin font fetching. If you serve multiple weights or styles, preload only the faces needed immediately by the story and make each preload URL match its corresponding CSS source.

A preload tells the browser to fetch the resource early; it does not repair a wrong URL, missing file, denied request, or incorrect font-face declaration. Inspect the network panel to confirm that the preload succeeded and was reused by the CSS request.

Fix 2: Serve font files from Storybook static assets

When practical, place the font files in Storybook’s static directory and reference them from the test stylesheet. This removes the external font host from the test path and makes availability easier to control. Your production site can retain its own delivery setup.

/* Example CSS; put the font file in Storybook's public/static asset directory. */
@font-face {
  font-family: "Brand Sans";
  src: url("/fonts/brand-regular.woff2") format("woff2");
  font-style: normal;
  font-weight: 400;
  font-display: swap;
}

body {
  font-family: "Brand Sans", Arial, sans-serif;
}

Use the static directory convention configured by your Storybook builder and verify the resulting public URL in the running preview. Keep the file format and path aligned with the actual build setup; the example path is illustrative, not a required Storybook configuration.

Fix 3: Wait for the required face with the Font Loading API

Preloading is usually the cleanest first step. If a story or global setup must not proceed until a specific face is available, a Storybook loader can await document.fonts.load(). The following example gates the wait to Chromatic and requests a regular face; change the CSS font shorthand and family to match the face the story uses.

// .storybook/preview.ts
import { isChromatic } from "@chromatic-com/storybook";
import type { Preview } from "@storybook/react";

const preview: Preview = {
  loaders: [
    async () => {
      if (isChromatic() && typeof document !== "undefined") {
        await document.fonts.load('400 16px "Brand Sans"');
      }
      return {};
    },
  ],
};

export default preview;

Use the Chromatic package and setup appropriate to your project version, and keep the loader in the preview configuration supported by your Storybook version. The important browser operation is document.fonts.load(); the gating helper is shown because Chromatic documents this pattern. If your project does not use that helper, apply an equivalent environment check or await the face in the relevant setup.

To wait for fonts used in the current document’s layout rather than naming one face, await document.fonts.ready:

// In a Storybook loader or other browser-side setup:
await document.fonts.ready;

document.fonts.ready resolves after fonts used by the document have loaded and layout operations are complete. It does not guarantee that every declared but unused face has loaded. The MDN FontFaceSet.ready, CSS Font Loading API, and Document.fonts references describe these browser APIs.

Fix 4: Make interaction tests start after the font is ready

Chromatic interactions begin as soon as the DOM loads. If a play function clicks, measures, or asserts against content whose layout depends on a web font, preload the font before the interaction begins. A loader that awaits the required face is more directly tied to readiness than a guessed delay.

Chromatic suggests adding a delay when preloading is not possible. Treat that as a fallback: a fixed delay can still be too short on a slow run and unnecessarily long on a fast one. If you use one, keep it local to the affected story or setup and confirm that the font request itself succeeds.

Choose a fix that matches the test’s purpose

Approach Reliability Typography fidelity Use when
Local static font plus preload High when the asset is included and URL is correct Preserves the intended face You need repeatable snapshots with the product font
document.fonts.load() Explicit for the requested face, assuming it can load Preserves the intended face A story must wait for a particular weight/style
document.fonts.ready Waits for fonts used by current document layout Preserves used faces Several used faces may affect the page
Web-safe fallback stack Resilient if the custom font is unavailable May change metrics and appearance Brand typography is not part of the visual contract
font-display: optional in Chromatic Reduces dependence on waiting for the custom face May render the fallback instead Fallback rendering is acceptable for this test

Chromatic documents font-display: optional as an option in its font loading guidance. Use it only when a fallback-font rendering is acceptable; otherwise the snapshot may stop representing the intended typography. Keep a suitable fallback in the font stack. Chromatic gives examples such as Arial, Verdana, and Trebuchet MS for sans-serif; Georgia and Times New Roman for serif; and Courier New or Courier for monospace. Select fallback coverage suitable for the languages your UI displays.

Verify the fix

  1. Confirm the expected font file request succeeds in the Storybook preview and in the Chromatic build.
  2. Confirm the requested URL, weight, style, and family match the preload and font-face declarations.
  3. Run the affected story under a cold-cache condition and inspect whether layout shifts before capture or interaction.
  4. For loader-based waiting, confirm the loader completes and that the intended face is available before dependent story work proceeds.
  5. Re-run the failed snapshot or interaction and check the trace for a remaining late or failed resource.

Do not use a successful warm-cache run as the only evidence: the original issue may be a resource timing race.

Common errors and fixes

Symptom Likely cause Fix
Preload appears unused The preload URL or request mode does not match the CSS font request Use the exact requested URL and the font preload attributes; inspect the network panel for duplicate requests
Font request is 404 or redirects unexpectedly Incorrect public path or asset not included in the Storybook build Verify the built static asset URL and update the CSS and preload to that path
Font works locally but not in Chromatic The asset depends on local files, a restricted host, or network access unavailable to the build Serve it with Storybook’s static assets or make the host reachable to the test browser
Loader finishes but fallback remains The font shorthand or family does not match the requested face, or the font failed to load Match family, weight, and style to the CSS and verify the underlying request and font-face declaration
Snapshot still shifts after waiting Another face, late stylesheet, or post-load layout change is involved Inspect all font requests and resource warnings; wait for the actually used face and investigate other asynchronous rendering
Interaction hits the wrong target Text reflow changes element position after the play function begins Preload before interactions or await the relevant face in setup; avoid relying on a timing guess
Fallback looks different across environments Fallback availability or glyph coverage differs Use a deliberate fallback stack with language-appropriate coverage, or load the intended font deterministically

Performance, reliability, and cost notes

Preloading can move a required font request earlier and reduce the chance that capture races the fetch. Avoid preloading every font file: preload only faces needed for the first render, because unnecessary preloads compete for network capacity. Local static assets remove dependence on an external font host for the test but still need to be present at the expected path.

Explicit font waits improve sequencing, but they cannot make a missing or blocked asset succeed. A loader also adds a wait to each applicable render, so scope it to Chromatic or to stories that depend on the face when possible. Fixed delays spend time without proving resource readiness. The practical cost of a late font is usually a failed or repeated visual test and time spent diagnosing unstable output; Chromatic’s documented render and interaction windows are limits, not a substitute for controlling the resource.

Or skip the browser setup

If your goal is to capture a rendered page rather than stabilize a Storybook test, ScreenshotNeo provides a website screenshot API and MCP server. A screenshot API does not fix Chromatic’s font-loading behavior or replace a visual regression test, but it can remove the browser capture setup when you need a page image.

One GET request returns an image or PDF. The API accepts options for full-page capture, element selection, viewport and device presets, retina scale, waits, custom CSS or JavaScript, and other capture settings. See the ScreenshotNeo API documentation for parameters.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo accepts cookie and consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers say which page verdict and billing result applied. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture.

The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free, and every feature is on every plan. Sign up for 1,000 free screenshots a month with no card.

FAQ

Does waiting for document.fonts.ready load every font declared in CSS?

No. It resolves after fonts used by the document and associated layout work are ready; unused declared faces may not be fetched.

Should I disable the custom font in Chromatic?

Only if the fallback appearance is acceptable for the story’s purpose. If typography is part of what the snapshot checks, keep the intended face and make its loading deterministic.

Will increasing Chromatic’s timeout fix a font URL that fails?

No. First establish that the requested resource is reachable and returns the expected font. More time does not correct a missing asset or blocked host.

Can ScreenshotNeo make a Chromatic test pass?

No. It is a separate website screenshot API; it can capture a page without browser automation setup, but it does not control Chromatic’s Storybook render or interaction lifecycle.