What SnapDrift is
SnapDrift is a free, MIT-licensed visual regression testing runner for GitHub Actions. It captures full-page frames of your application, compares them against a known baseline, and reports drift directly on the pull request. It is the open-source client behind Snap's hosted Visual CI, and it runs the same comparison engine as the free in-browser visual diff tool.
You keep ownership of checkout, build, startup, readiness, and teardown. SnapDrift takes over once your app is reachable — so it fits the CI you already have instead of asking you to reorganize it.
How it works
Every pull request runs the same loop, driven by two workflow jobs and a single config file:
Quickstart
Two files get you from zero to a reviewed visual run. First, add .github/snapdrift.json:
{
"baselineArtifactName": "my-app-snapdrift-baseline",
"workingDirectory": ".",
"baseUrl": "http://127.0.0.1:8080",
"resultsFile": "qa-artifacts/snapdrift/baseline/current/results.json",
"manifestFile": "qa-artifacts/snapdrift/baseline/current/manifest.json",
"screenshotsRoot": "qa-artifacts/snapdrift/baseline/current",
"routes": [
{ "id": "home-desktop", "path": "/", "viewport": "desktop" },
{ "id": "home-mobile", "path": "/", "viewport": "mobile" }
],
"diff": { "threshold": 0.01, "mode": "report-only" }
}Then wire up the two jobs — one publishes the baseline from main, the other runs on every pull request. Both are pinned to the immutable SnapDrift v0.13.0 release commit:
name: Visual Regression
on:
push:
branches: [main]
pull_request:
# pr-diff posts its report as a PR comment, so the token needs write access
# to issues and pull requests.
permissions:
contents: read
actions: read
issues: write
pull-requests: write
jobs:
baseline:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# ... check out, build and start your app ...
- name: SnapDrift Baseline
uses: ranacseruet/snapdrift/actions/baseline@3967590687d420479ba5ec351e1394de174c1838 # v0.13.0
with:
repo-config-path: .github/snapdrift.json
pr-diff:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# ... check out, build and start your app ...
- name: SnapDrift Report
uses: ranacseruet/snapdrift/actions/pr-diff@3967590687d420479ba5ec351e1394de174c1838 # v0.13.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
repo-config-path: .github/snapdrift.jsonThe full walkthrough — permissions, route scoping, and the hosted provider — is in the Visual CI guide.
Enforcement modes
Set diff.mode to control what a drifting run does. Start permissive and tighten once the signal is stable:
| Mode | Behavior | When to use |
|---|---|---|
report-only | Never stops the run; posts results for visibility | Start here while baselines settle |
fail-on-changes | Fails when any capture exceeds the threshold | Day-to-day once the signal is stable |
fail-on-incomplete | Fails on missing captures or comparison errors | Catch broken or partial captures |
strict | Fails on any drift signal or incomplete comparison | Lock the UI down before a release |
What it handles
Baselines on your default branch
A baseline job runs when you merge to main and stores the accepted screenshots. Every pull request then diffs against the latest successful baseline, so drift is measured against what actually shipped.
Pull-request reporting
SnapDrift posts an upserted PR comment with the routes that changed, the diff result, and (with the hosted provider) a link into the review dashboard. No scrolling through raw CI logs to find what moved.
Route scoping from changed files
Map source paths to routes so a pull request only captures the pages it touches. Smaller runs, faster feedback, and fewer unrelated diffs on every comment.
Four enforcement modes
Start at report-only while baselines settle, then move to fail-on-changes, fail-on-incomplete, or strict as the signal becomes trustworthy.
Pluggable backends
Local filesystem by default, or provider: "snap" for hosted Snap baselines. Switch with one config change — the routes and diff settings carry over.
Export baselines, no lock-in
snapdrift migrate-baselines --to local --from snap downloads the project's hosted baselines into the local layout for reproducible debugging or to leave the hosted provider. Adopting the hosted provider is a deliberate step — seed it from the baseline action — not a re-upload of a local bundle.
Local CLI
SnapDrift also ships a CLI for running captures, diffs, and migrations locally against a running app — useful for validating a change before you push:
# Establish a local-provider baseline
snapdrift baseline
# Capture screenshots without publishing anything
snapdrift capture
# Compare against the baseline after making UI changes
snapdrift diff --open
# Export hosted baselines back to the local layout
snapdrift migrate-baselines --to local --from snapCurrent constraints
- Ubuntu runners for the actions; the local CLI runs on any OS Node 22+ supports.
- Full-page capture only.
- Viewport presets
desktop(1440×900) andmobile(390×844), or a custom{ width, height }. - One global
diff.threshold. - Unequal dimensions compare on a top-left-aligned union canvas, so a size change still produces a reviewable diff instead of failing outright.
Free forever, or scale without the runner
SnapDrift is free because it runs on your infrastructure. When running Playwright inside CI starts to cost more than it is worth, Snap's hosted provider is a drop-in upgrade: change provider to "snap" and Snap renders your preview deploy on its Lambda fleet, keeps baselines in S3 that don't expire, and adds a review dashboard — the same routes, the same diff config, no workflow rewrite.
Try the engine for free first. The in-browser visual diff tool uses the same comparison engine as SnapDrift — drop in two images and see exactly what a run reports. Need raw captures instead of diffs? The free screenshot API captures any public URL with no signup.
Frequently asked questions
Is SnapDrift really free?
Yes. SnapDrift is MIT-licensed and free to run in your own GitHub Actions. There is no account, no API key, and no per-screenshot charge for local mode. You bring the runner; SnapDrift handles capture, diff, and reporting.
Does SnapDrift need a Snap account?
No. Local mode works with just the two workflow jobs and a .github/snapdrift.json file. A Snap account is only needed if you opt into provider: "snap" for hosted baselines and Lambda-rendered captures.
How is this different from Snap's hosted Visual CI?
SnapDrift is the open-source client you run yourself: it installs Playwright on your runner, starts your app, and keeps baselines as GitHub artifacts. Hosted Visual CI is the managed upgrade — Snap renders your preview deploy on its Lambda fleet, stores baselines in S3 that don't expire, and adds a review dashboard. Same workflow, less infrastructure to own.
When should I move from SnapDrift to the hosted provider?
When baseline artifacts expiring (30 days by default) becomes a problem, when installing Playwright and starting your app on every run costs too many runner minutes, or when you want reviewers to triage and approve diffs in a dashboard instead of PR comments.
Does SnapDrift work outside GitHub Actions?
The local CLI (snapdrift capture, diff, and migrate-baselines) runs anywhere Node 22+ is supported, so you can validate UI changes during development. The action-based baseline and PR-diff pipelines are built for GitHub Actions, and the hosted provider path is designed for multiple CI systems.