Configuring Chromatic Threshold Settings for Pixel-Perfect Diffs

Problem statement

Your Chromatic build fails every time someone touches a gradient button or a shadow card, even though no visual change was intentional. Alternatively, a subtle flex-container misalignment sailed through review because your tolerance was set too loosely. Both symptoms have the same root: the diffThreshold value does not match the rendering complexity of your components.

Root cause explanation

Chromatic’s diff engine calculates a normalized perceptual pixel distance — not a raw pixel count. Each pixel’s RGB delta is weighted by luminance impact and spatial proximity, so a single high-contrast edge shift near a button label scores higher than dozens of low-contrast sub-pixel changes in a blurred shadow. The default threshold of 0 treats every imperceptible rasterisation difference as a regression.

This interacts directly with the concepts covered in Tolerance Thresholds: the platform offers per-story overrides, which means a blanket global value is almost always miscalibrated for at least some of your component types. Hardware-accelerated compositing, OS-level font hinting, and GPU driver differences all generate cosmetic variance that does not represent a real UI change — variance the threshold must absorb without masking layout regressions.

Threshold decision diagram

The diagram below maps the three calibration paths from a failing diff to the right fix.

Chromatic threshold calibration decision flow A flowchart starting at "Diff fails in CI" and branching through two questions — "Edge/text sub-pixel shift?" and "Animation frame captured?" — to three outcomes: raise diffThreshold, add includeAntiAliasing, and set pauseAnimationAtEnd or disable the story. Diff fails in CI Edge / text sub-pixel shift? Yes Set includeAntiAliasing: false No Animation frame captured? Yes Set pauseAnimationAtEnd: true No Raise diffThreshold per component type

Minimal reproduction

This global configuration triggers false positives on any component that uses gradients, shadows, or web fonts:

// .storybook/preview.js  — bare minimum that causes noise
export const parameters = {
  chromatic: {
    diffThreshold: 0,      // zero tolerance rejects all sub-pixel variance
  },
};

Run the build and observe the failure count:

npx chromatic --project-token=chpt_abc123def456 --branch-name=main
# Build 142: 47 changes detected, 0 accepted — pipeline blocked

Step-by-step fix

1. Replace the global default with a calibrated baseline

Set a permissive-but-not-blind global value and disable anti-aliasing noise:

// .storybook/preview.js
// Establishes a sensible global baseline that absorbs sub-pixel rendering variance
export const parameters = {
  chromatic: {
    diffThreshold: 0.01,        // 1% perceptual distance — absorbs sub-pixel noise
    includeAntiAliasing: false, // filters fractional edge samples across the snapshot
    pauseAnimationAtEnd: true,  // captures the final animation frame, not a mid-frame state
    viewports: [375, 1440],     // standardised mobile + desktop breakpoints
  },
};

2. Apply per-story overrides for visually complex components

Use the parameters.chromatic object inside each story to loosen tolerance only where the rendering pipeline genuinely demands it:

// src/components/HeroBanner/HeroBanner.stories.js
// Gradient backgrounds require a higher diffThreshold due to GPU interpolation variance
export const GradientHero = {
  args: { variant: 'gradient-primary' },
  parameters: {
    chromatic: {
      diffThreshold: 0.05,      // accepts GPU-specific gradient rasterisation differences
      pauseAnimationAtEnd: true,
      viewports: [1440],
    },
  },
};

// Canvas charts are non-deterministic — exclude from pixel diffing entirely
export const LiveRevenueChart = {
  parameters: {
    chromatic: { disable: true },
  },
};

The following table maps component types to recommended threshold ranges:

Component type Recommended diffThreshold Rationale
Typography, icons 0.0050.01 High-contrast edges; sub-pixel shifts are visually irrelevant
Flat UI, cards 0.010.02 Standard layout boundaries; minimal GPU compositing
Gradients, shadows 0.030.05 Engine-specific blur and interpolation variance
Canvas, WebGL disable: true Non-deterministic rendering; test state, not pixels

3. Freeze animations and dynamic content

Chromatic captures a snapshot at a specific moment. Any CSS transition, @keyframes animation, or JavaScript-driven interval can land the snapshot mid-frame. Apply a blanket motion freeze using prefers-reduced-motion:

/* src/styles/test-utils.css */
/* Forces all animations to complete in one imperceptible tick during visual tests */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

Wire it in via a Storybook global decorator so it runs for every story during visual capture:

// .storybook/preview.js
import '../src/styles/test-utils.css';

// Wraps every story in a class that activates reduced-motion CSS
export const decorators = [
  (Story) => (
    <div data-testid="chromatic-wrapper">
      <Story />
    </div>
  ),
];

4. Lock the CI pipeline to reject unapproved diffs

Threshold calibration is pointless without a hard gate. Chromatic’s CLI exits non-zero on unapproved changes; expose that signal to your branch protection rules:

# .github/workflows/visual-regression.yml
name: Visual Regression
on:
  pull_request:
    branches: [main, develop]

jobs:
  chromatic:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0          # full history required for baseline resolution
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - name: Chromatic visual diff
        uses: chromaui/action@v11
        with:
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
          buildScriptName: build-storybook
          autoAcceptChanges: false   # never silently accept — require explicit review
          exitZeroOnChanges: false   # non-zero exit = PR blocked until approved
          onlyChanged: true          # only snapshot stories affected by this PR's files
          branchName: ${{ github.head_ref }}

For design-token maintenance branches that intentionally update many baselines, scope autoAcceptChanges to branch name rather than disabling CI gating entirely:

# ci/chromatic.sh — branch-scoped auto-accept
BRANCH="${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}"

if [[ "$BRANCH" == chore/update-tokens* ]]; then
  npx chromatic --project-token=chpt_abc123def456 --auto-accept-changes --branch-name="$BRANCH"
else
  npx chromatic --project-token=chpt_abc123def456 --branch-name="$BRANCH"
fi

Verification

After applying the fixes, re-run the build. A correctly calibrated pipeline produces output like:

✔ Storybook build in 48s
✔ 312 stories captured (47 changed from baseline)
✔ 0 diffs above threshold
✔ Build 143: no changes detected — pipeline passed

If diffs above threshold still appear, inspect the per-story breakdown with --diagnostics to see each component’s perceptual distance score and identify which story needs a more specific override.

Edge cases / caveats

  • Safari and color-managed displays. macOS with wide-gamut (P3) display profiles causes Safari to apply aggressive color management, producing larger perceptual scores on any color-critical story. If your matrix includes WebKit, widen gradient and image thresholds by 0.010.02 beyond your Blink baseline, or restrict Safari snapshots to shape-only components.
  • Web font loading races. If @font-face fonts are not preloaded before Chromatic captures the snapshot, the fallback-to-custom-font swap generates a layout shift that no threshold adjustment can cleanly absorb. Add <link rel="preload" as="font"> for every custom typeface and confirm document.fonts.ready resolves before relying on diffThreshold to handle the variance.
  • Storybook parameters inheritance. Story-level parameters.chromatic merges with the global default rather than replacing it, so setting diffThreshold: 0.05 at story level while includeAntiAliasing: false is set globally means both apply. Verify the merged config with Storybook’s @storybook/addon-interactions panel before assuming a story-level threshold is standing alone.

FAQ

What is the difference between diffThreshold and threshold in older Chromatic versions?

threshold was the pre-6.x parameter name. Both accept the same 01 normalized perceptual distance scale, but the current Chromatic SDK only reads diffThreshold. If your stories use threshold, migrate them — the old key is silently ignored in recent builds.

Does setting diffThreshold: 0.05 mean 5% of pixels can change?

No. It means the normalized perceptual distance score for the diff can be up to 0.05. A single high-contrast pixel shift near a text label can score 0.03 on its own, while hundreds of low-contrast sub-pixel changes in a blurred shadow might score under 0.01. The scale is luminance-weighted, not a raw pixel percentage.

Can I audit which components have drifted above 0.05 across builds?

Yes — Chromatic exposes build and snapshot data via its GraphQL API. Query builds(projectId: "...") and filter snapshots by diffPercentage > 0.05 to surface candidates for CSS refactoring or threshold reset after fixing the root rendering cause.


  • Tolerance Thresholds — parent page covering the full tolerance calibration workflow, including snapshot baseline management and cross-browser delta strategies.
  • Visual Regression & Snapshot Strategies — overview of the complete visual testing discipline: baseline lifecycle, diff algorithms, and CI integration patterns.
  • Pixel Diff Algorithms — how perceptual hashing and SSIM compare to Chromatic’s normalized distance model when choosing a diff strategy.
  • Baseline Management — workflows for approving, rejecting, and branching snapshot baselines as your design system evolves.
  • Cross-Browser Matrix — structuring Blink, WebKit, and Gecko capture targets to control the rendering variance that drives threshold decisions.