CI/CD

Font rendering differences in visual tests

Fonts are the silent killer of visual test stability. Learn why the same font looks different across environments and how to achieve consistent rendering.

What causes font differences

Font rendering is surprisingly complex. The same font file can produce visually different output depending on the operating system, browser, display, and configuration.

OS rasterizers differ

macOS, Windows, and Linux use different font rendering engines. The same font file produces visually different output on each platform.

Fallback font substitution

When a font isn't available, the browser substitutes a fallback. Different systems have different fallbacks, causing visible differences.

Subpixel antialiasing

Subpixel rendering varies by display type and OS settings. ClearType on Windows differs from macOS subpixel rendering.

Font hinting differences

TrueType hinting instructions are interpreted differently across renderers. This affects character spacing and shape at small sizes.

These differences are subtle individually but compound across a page. A screenshot from macOS compared to Linux CI will show differences on nearly every text element.

Fixes that actually work

The goal is to ensure your screenshots are captured in an identical environment every time—regardless of who runs them or where.

Pin environment with Docker

Run visual tests in a Docker container with fixed OS and font configuration. This is the most reliable approach for CI.

Install consistent fonts

Ensure the same fonts are installed in all environments. Don't rely on system fonts that vary by OS.

Use web fonts

Web fonts are bundled with your app, ensuring consistency. But you must wait for them to load before capturing screenshots.

Force font smoothing

CSS font-smoothing properties can normalise rendering. Use antialiased rendering to reduce subpixel variance.

Pin your environment with Docker

The most reliable approach is to run visual tests inside a Docker container. This gives you complete control over the OS, font packages, and rendering configuration.

FROM mcr.microsoft.com/playwright:v1.40.0-jammy

# Install fonts your app uses
RUN apt-get update && apt-get install -y \
    fonts-liberation \
    fonts-noto-color-emoji \
    && rm -rf /var/lib/apt/lists/*

# Optional: install custom fonts
COPY ./fonts /usr/local/share/fonts
RUN fc-cache -fv

See Docker setup for deterministic rendering for a complete configuration guide.

Use web fonts and wait for loading

Web fonts are bundled with your application, ensuring the same font files are used everywhere. But you must wait for them to load before capturing screenshots.

In Playwright, wait for fonts explicitly:

// Wait for all fonts to load
await page.evaluate(() => document.fonts.ready);

// Or check specific fonts
await page.evaluate(async () => {
  await document.fonts.load('16px "Inter"');
});

Font loading can be slow on first page load. If you're seeing fallback fonts in screenshots, increase your wait time or add an explicit font load check.

Force consistent font smoothing

CSS can help normalise font rendering across platforms. Force antialiased rendering to reduce subpixel variance:

/* Add to your test environment CSS */
* {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: geometricPrecision;
}

This reduces but doesn't eliminate differences. Different operating systems still interpret these properties differently. It's most effective when combined with a pinned Docker environment.

Debugging font issues

When you see unexpected font diffs, here's how to identify the root cause:

  • Check computed styles: Use DevTools to verify which font is actually rendering. The computed font-family shows the resolved font, including fallbacks.
  • Compare font metrics: Capture the same text in both environments and overlay them. Font rendering differences manifest as slightly different character widths and heights.
  • Verify font loading: Check document.fonts.status and document.fonts.check() for specific fonts. FOUT (flash of unstyled text) suggests incomplete font loading.
  • Isolate the variable: Run the same test in Docker locally and in CI. If they match, the issue is local environment. If they differ, check Docker image consistency.

Font determinism checklist

  • Run visual tests in a consistent environment (Docker recommended)
  • Install required fonts explicitly in your CI environment
  • Use web fonts instead of system fonts where possible
  • Wait for document.fonts.ready before capturing screenshots
  • Set consistent font-smoothing CSS across all text
  • Generate baselines from CI, not local development machines
  • If using fallback fonts, verify they're identical across environments
  • Test that emoji and icon fonts render consistently

Related guides

Frequently Asked Questions

Why do my screenshots differ between macOS and Linux CI?
macOS and Linux use fundamentally different font rendering engines. macOS prioritises fidelity to the font design, while Linux (FreeType) prioritises on-screen readability. The same font will look noticeably different. The solution is to run visual tests on a single, consistent platform—usually Linux in Docker.
Should I commit baselines from my Mac or from CI?
Always commit baselines from CI. Your CI environment is what runs on every PR, so it's the source of truth. If you commit baselines from your Mac, they'll fail on every CI run. Treat CI as the canonical rendering environment.
How do I ensure web fonts are loaded before screenshots?
Use the document.fonts.ready promise or Playwright's built-in font loading detection. In Playwright, you can wait for networkidle or explicitly check document.fonts.status. For critical accuracy, add a visual check that a known character renders correctly.
Can I use system fonts and still have consistent tests?
Only if you run tests on a single platform with controlled font installation. System fonts vary wildly across OS versions and configurations. For reliability, either use web fonts or pin your test environment with Docker and explicit font packages.
What's the easiest fix for font differences?
Run all visual tests in Docker with a fixed Linux image. Install the fonts your app uses in the Docker image. Generate baselines from CI (not locally). This removes local machine variance from the equation entirely.
Do font differences affect pixel-diff thresholds?
Yes, significantly. Font rendering differences often affect every text element on the page. If you're seeing large pixel diff percentages across the entire page, fonts are likely the cause. A threshold high enough to ignore font differences will miss real bugs.

Eliminate font rendering headaches from visual testing

Get early access