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.
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.005–0.01 |
High-contrast edges; sub-pixel shifts are visually irrelevant |
| Flat UI, cards | 0.01–0.02 |
Standard layout boundaries; minimal GPU compositing |
| Gradients, shadows | 0.03–0.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.01–0.02beyond your Blink baseline, or restrict Safari snapshots to shape-only components. - Web font loading races. If
@font-facefonts 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 confirmdocument.fonts.readyresolves before relying ondiffThresholdto handle the variance. - Storybook
parametersinheritance. Story-levelparameters.chromaticmerges with the global default rather than replacing it, so settingdiffThreshold: 0.05at story level whileincludeAntiAliasing: falseis set globally means both apply. Verify the merged config with Storybook’s@storybook/addon-interactionspanel 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 0–1 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.
Related
- 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.