Unit tests don't notice a button pushed off screen, a font that failed to load or a CSS change that broke the checkout on mobile. Visual regression tests do: screenshot the important pages on every pull request, compare them with the last approved version, and flag the pixels that changed. The hard part is getting screenshots that are identical when nothing changed. That's what most of the options below are for.
A deterministic screenshot
Anything that varies between runs shows up as a false positive. Freeze animations, block third parties and hide dynamic content:
curl -X POST "https://shotkit.net/api/take" \
-H "X-Access-Key: YOUR_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://preview-1234.example.com/pricing","viewport_width":1280,"viewport_height":800,"full_page":true,"format":"png","reduce_motion":true,"block_ads":true,"block_trackers":true,"block_chats":true,"block_cookie_banners":true,"hide_selectors":[".testimonial-carousel","[data-testid=relative-time]"],"time_zone":"UTC","wait_until":["networkidle0"],"fail_if_content_missing":["Pricing"]}' \
--fail-with-body -o shot.pngreduce_motionfinishes CSS animations and transitions and pauses videos.- Blocking ads, trackers and chat widgets removes the content you don't control.
hide_selectorshides carousels, relative times, avatars and anything else that changes by itself.time_zonekeeps dates the same wherever the test runs.fail_if_content_missingturns a broken deploy into an error instead of a screenshot of an error page.
Always use PNG: lossy formats add compression noise that a pixel diff reports as changes.
Diff against the baseline
Store the approved screenshots (from your main branch) and compare each new one with pixelmatch:
import { PNG } from "pngjs";
import pixelmatch from "pixelmatch";
const before = PNG.sync.read(baselineBuffer);
const after = PNG.sync.read(newBuffer);
const { width, height } = before;
const diff = new PNG({ width, height });
const changed = pixelmatch(before.data, after.data, diff.data, width, height, { threshold: 0.1 });
if (changed / (width * height) > 0.001) {
// more than 0.1% of pixels changed: fail the check and upload diff.png for review
}Full-page screenshots change height when content is added. Compare only when sizes match, and treat a size change as a change. odiff is a faster alternative for large images.
Test components in Storybook
Point at Storybook's iframe URL for one story and capture its root element. Each story becomes a tightly cropped, stable image:
curl -X POST "https://shotkit.net/api/take" \
-H "X-Access-Key: YOUR_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://storybook.example.com/iframe.html?id=button--primary&viewMode=story","selector":"#storybook-root","format":"png","device_scale_factor":2,"reduce_motion":true}' \
--fail-with-body -o shot.pngCompared to Percy, Chromatic and Applitools
Those services include a review UI, baselines per branch and team approvals, and charge per screenshot accordingly. Using an API for the capture and pixelmatch for the diff gives you the same core check, with your own storage and review flow, at screenshot API prices. Pick the hosted tools if you want the workflow; pick this if you want the building block.
Frequently asked questions
Why do my screenshots differ when nothing changed?
The usual causes are animations, web fonts arriving late, third-party widgets, carousels and relative dates. reduce_motion, wait_until: ["networkidle0"], the blockers and hide_selectors handle each of them.
Can I test pages that aren't public yet?
The page has to be reachable from the internet, such as a preview deployment. For protected previews, pass a token with authorization, a cookie with cookies, or a bypass header with headers.
Should I capture the full page or the viewport?
The viewport for quick checks of what users see first, the full page to catch problems anywhere. Full pages are bigger to diff and change height more often.
Can I test several screen sizes?
Yes. Run the same list with a few viewport_device presets; see responsive design screenshots.