Playwright anti-aliasing / sub-pixel screenshot differences in CI
Anti-aliasing smooths edges by blending pixels, and the exact blend can vary run to run or between GPUs. Playwright fails a screenshot on a single differing pixel by default, so set a small maxDiffPixels and raise threshold to absorb this sub-pixel noise.
What this error means
Screenshot tests fail intermittently with a handful of differing pixels along curved or diagonal edges. Re-running sometimes passes. The diff shows faint fringing on borders and text, not a layout change.
Error: A snapshot doesn't match its reference.
438 pixels (ratio 0.002 of all image pixels) are different.
(fringe along rounded corners and text edges)Common causes
Zero default tolerance on noisy renderers
With no maxDiffPixels set and a strict threshold, edges that anti-alias slightly differently exceed it and register as changed pixels. Shared CI runners vary in GPU and backend.
Unstable rendering: animations, carets, or scaling
A blinking caret, a mid-flight CSS animation, or a non-integer device scale factor produces different sub-pixel output on each capture.
How to fix it
Set tolerances and disable animations
Increase threshold, allow a small maxDiffPixels, and let Playwright freeze CSS animations during the capture.
await expect(page).toHaveScreenshot('card.png', {
threshold: 0.2,
maxDiffPixels: 100,
animations: 'disabled',
caret: 'hide',
});Set expect defaults once and pin device scale
Configure toHaveScreenshot options in the config so every screenshot shares a sane tolerance, and pin an integer deviceScaleFactor to avoid sub-pixel resampling.
export default defineConfig({
use: { deviceScaleFactor: 1 },
expect: { toHaveScreenshot: { maxDiffPixels: 100, threshold: 0.2 } },
});How to prevent it
- Set
animations: 'disabled'andcaret: 'hide'for screenshot tests. - Configure
maxDiffPixels/thresholdglobally, not per test. - Use a whole-number
deviceScaleFactor.