Jest snapshots vs Playwright visual diff vs Chromatic
This page sits in the Visual Testing Tool Comparisons section of the Visual Regression & Snapshot Strategies guide. It answers one decision: when you want to stop a Button component from regressing visually, which of the three most common tools should you reach for — and what does each one silently fail to catch?
Problem statement
Teams reach for “snapshot testing” as if it were one thing. It is not. Jest’s toMatchSnapshot, Playwright’s toHaveScreenshot, and Chromatic all claim to catch regressions, but they operate at completely different layers and catch completely different classes of change.
Pick wrong and you get a false sense of safety. The most common failure: a team adds Jest snapshots to their component library, watches the green checkmarks, and ships a release where every button’s background color shifted from #2563eb to #1e40af because a design token changed. Every Jest snapshot passed — the serialized DOM was byte-for-byte identical — because Jest never rendered the CSS. The regression sailed into production.
The inverse failure is over-buying. A small team wires up Chromatic for a five-component internal admin panel, then gets a bill scaled to snapshot count they never budgeted for, when a handful of self-hosted Playwright screenshots would have covered the same surface.
Root cause
The three tools disagree about what a “snapshot” is. This is a tolerance and diff-algorithm distinction at heart: Jest diffs a text serialization of the DOM, Playwright and Chromatic diff rasterized pixels of a real rendered browser frame.
A Jest snapshot is a string. It records that the element is a <button class="btn btn--primary"> with certain children — but the class name is just text to Jest. Whether .btn--primary resolves to blue or green, whether the padding collapsed, whether a web font failed to load — none of that is in the serialization, so none of it can diff. Playwright and Chromatic run the component in Chromium (and other engines), apply the real stylesheet, and compare the resulting image, so they see color, spacing, and font rendering that Jest is structurally blind to.
Minimal reproduction
Take one Button and change only its CSS. Watch which tool notices.
// Button.tsx — the component under test
export function Button({ variant = 'primary', children }: {
variant?: 'primary' | 'secondary';
children: React.ReactNode;
}) {
return <button className={`btn btn--${variant}`}>{children}</button>;
}
/* button.css — a design-token change lands here, markup unchanged */
.btn--primary { background: #2563eb; /* was #2563eb, PR changes it to #1e40af */ }
The DOM output is identical before and after — same tag, same class, same children. A Jest snapshot cannot see the color at all. Only a rendered-pixel tool will diff it.
Step-by-step fix
Run the same Button through all three tools so you can see, concretely, what each reports.
1. Jest serialized snapshot — the DOM contract
// Button.test.tsx
import { render } from '@testing-library/react';
import { Button } from './Button';
test('primary button DOM contract', () => {
const { container } = render(<Button variant="primary">Save</Button>);
expect(container.firstChild).toMatchSnapshot();
});
// Button.test.tsx.snap — the stored artifact
exports[`primary button DOM contract 1`] = `
<button class="btn btn--primary">Save</button>
`;
What this does: it locks the rendered markup and attributes. If a refactor drops the btn--primary class or reorders children, the diff fails loudly. Change the background color from #2563eb to #1e40af and this test stays green — proving Jest snapshots are a structural guard, not a visual one.
2. Playwright pixel screenshot — the self-hosted visual guard
// button.spec.ts
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';
test('primary button renders correctly', async ({ mount }) => {
const component = await mount(<Button variant="primary">Save</Button>);
await expect(component).toHaveScreenshot('button-primary.png', {
maxDiffPixelRatio: 0.01, // tolerance — see the tolerance-thresholds guide
});
});
What this does: Playwright mounts the component in real Chromium, applies the stylesheet, and rasterizes the result to button-primary.png under __screenshots__/. Now the color change from step 1’s reproduction produces a failing pixel diff. You own the baseline PNG and the CI runner — no per-snapshot fee, but you maintain browser binaries and font-rendering consistency yourself.
3. Chromatic — the cloud review workflow
// .storybook/Button.stories.jsx
import { Button } from '../src/Button';
export default { component: Button, title: 'Button' };
export const Primary = { args: { variant: 'primary', children: 'Save' } };
export const Secondary = { args: { variant: 'secondary', children: 'Cancel' } };
# publish the Storybook to Chromatic's cloud renderer
npx chromatic --project-token=chpt_a1b2c3d4e5f6 --exit-zero-on-changes
What this does: Chromatic builds your Storybook, uploads it, and renders each story on its own hosted browsers. Every story becomes a snapshot; the color change surfaces in Chromatic’s web UI as a side-by-side diff a reviewer clicks to accept or reject. You pay per snapshot, but you get cross-browser capture and a baseline-branching review flow without running any infrastructure. Wire it into CI as a required status check so unreviewed diffs cannot merge.
4. Choose by scenario
| Concern | Jest snapshot | Playwright toHaveScreenshot |
Chromatic |
|---|---|---|---|
| What it diffs | Serialized DOM text | Rendered pixels (local) | Rendered pixels (cloud) |
| Catches CSS/token regressions | No | Yes | Yes |
| Catches markup/prop changes | Yes | Indirectly | Indirectly |
| Cross-browser capture | N/A | You configure engines | Built in |
| Infrastructure to run | None (in-process) | You host browsers + CI | Hosted for you |
| Baseline storage | .snap in repo |
PNG files in repo | Cloud, branch-aware |
| Review workflow | Code review of .snap |
Code review of PNG | Dedicated approval UI |
| Direct cost | Free | CI compute only | Per snapshot billed |
| Best fit | DOM contract in unit tests | A few critical states, self-hosted | Whole story catalog + design review |
Recommendation by scenario:
- A component library with a design-token pipeline and many variants → Chromatic. The per-story review UI and baseline branching pay for themselves when designers need to approve intentional visual changes.
- A product team that wants pixel coverage on a dozen key screens without a subscription → Playwright. Self-hosted, no per-snapshot fee, full control of the diff tolerance.
- A library that mainly needs to lock its public DOM/prop contract → Jest snapshots, cheap and instant — but never rely on them alone for visual correctness.
- A mature design system → all three, layered: Jest for the contract, Playwright for a few self-hosted critical states, Chromatic for the full catalog review.
Verification
Prove the CSS-blindness of Jest and the pixel-awareness of the others by changing only the color and re-running each.
# 1. Jest — passes despite the colour change (structural only)
npx jest Button.test.tsx
# PASS ./Button.test.tsx ✓ primary button DOM contract (12 ms)
# 2. Playwright — fails, naming the pixel delta
npx playwright test button.spec.ts
# 1) button.spec.ts:5 › primary button renders correctly
# Error: Screenshot comparison failed:
# 1843 pixels (ratio 0.03) exceed maxDiffPixelRatio 0.01
# Expected: button-primary.png Received: button-primary-actual.png
# 3. Chromatic — reports the change in the review UI
npx chromatic --project-token=chpt_a1b2c3d4e5f6
# ✓ Build 42 published
# → 1 change must be reviewed: Button › Primary
The contrast is the whole point: Jest’s green checkmark next to Playwright’s and Chromatic’s red is exactly the false-confidence trap that picking the wrong tool creates.
Edge cases and caveats
- Jest inline snapshots still can’t see CSS. Switching from
toMatchSnapshottotoMatchInlineSnapshotonly moves the serialized string into the test file — it changes ergonomics, not what is captured. It remains blind to rendered styles. - Playwright screenshots are flaky across OS and font stacks. A baseline PNG captured on macOS will diff against a Linux CI runner because of sub-pixel font hinting. Pin the browser version, run baselines in a container that matches CI, and consult font-rendering differences across browsers before trusting the tolerance.
- Chromatic snapshot counts multiply fast. Snapshots equal stories times viewports times browsers. A 40-story library at three viewports and two browsers is 240 snapshots per build — budget against your commit frequency, not your story count.
FAQ
Do Jest snapshots catch CSS regressions?
No. Jest’s toMatchSnapshot serializes the rendered DOM tree as text — element names, attributes, and children. It never applies a stylesheet or rasterizes pixels, so a change to a color, padding, or border-radius that leaves the markup identical produces no diff. To catch visual CSS changes you need a tool that renders in a real browser, such as Playwright’s toHaveScreenshot or Chromatic.
Is Playwright visual diff free compared to Chromatic?
Playwright itself is open source with no per-snapshot fee, but it is not free of cost. You run and maintain the browser infrastructure, store baseline PNGs in your repository, and manage font and rendering consistency across CI runners. Chromatic charges per snapshot but absorbs the rendering infrastructure, cross-browser capture, and the review workflow. The trade is engineering time versus a subscription.
Can I use all three tools together on the same component?
Yes, and mature design systems often do. Jest snapshots guard the DOM contract in unit tests, Playwright pixel diffs cover a few critical rendered states self-hosted, and Chromatic reviews the full story catalog across browsers. Layer them by cost and coverage rather than picking one — each catches a class of regression the others miss.
Related
- Visual Testing Tool Comparisons — the parent section weighing the visual testing tools this page compares head to head.
- Percy vs Chromatic for Component Visual Testing — a deeper comparison of the two cloud services once you have ruled in a hosted workflow.
- Configuring Chromatic Threshold Settings for Pixel-Perfect Diffs — how to tune diff sensitivity so pixel tools flag real changes without noise.
- Handling Font Rendering Differences Across Browsers — why Playwright baselines drift between machines and how to stabilize them.
- Making Visual Review a Required Status Check — gate merges on an approved diff so a visual regression cannot slip through review.