Local-first visual PR diffs for Next.js and Vite apps.
pr-visual-diff compares your current branch against a base branch, captures screenshots across configured routes and viewports, generates pixel diffs, and writes a static HTML report you can inspect locally before pushing.
- Uses temporary Git worktrees instead of checking out branches in your current working copy.
- Builds and starts the app in production-like mode for more stable screenshots.
- Captures desktop and mobile screenshots with Playwright.
- Generates
before.png,after.png,diff.png, plus a localreport.html. - Supports an app-owned auth/setup hook for bypass cookies, headers, or deterministic seeding.
- Officially supports Next.js 13+ and React + Vite
- Chromium only
- Intended for 10 routes or fewer
- Best for deterministic routes with limited animation and live data
- No CI, GitHub integration, Storybook, baseline approval flows, or cloud hosting in V1
npm install -D pr-visual-diff- Initialize config:
npx pr-visual-diff init- Edit
.visualdiff.json:
{
"baseBranch": "origin/main",
"framework": "next",
"installCommand": "npm install",
"buildCommand": "npm run build",
"startCommand": "npm run start",
"port": 3000,
"readyUrl": "http://127.0.0.1:3000",
"outputDir": ".visual-diff",
"routes": [
"/",
{ "path": "/dashboard", "expectUrl": "/dashboard" }
],
"viewports": [
{ "name": "desktop", "width": 1440, "height": 900 },
{ "name": "mobile", "width": 390, "height": 844 }
],
"capture": {
"headless": true,
"settleMs": 1200,
"readyTimeoutMs": 60000,
"disableAnimations": true,
"maskSelectors": [],
"headers": {}
},
"diff": {
"threshold": 0.1,
"failOnChange": false
},
"auth": {
"setupScript": ""
}
}- Check your environment:
npx pr-visual-diff doctor- Run the comparison:
npx pr-visual-diff run- Open
.visual-diff/report.html.
Creates .visualdiff.json using detected framework defaults.
npx pr-visual-diff initValidates local prerequisites and config shape.
npx pr-visual-diff doctorRuns the full branch-to-branch visual comparison.
npx pr-visual-diff run --base origin/main --routes /,/dashboard --headless --verboseOptions:
--base <branch>--routes <csv>--headless--no-headless--verbose--config <path>--output <dir>--fail-on-change
If your app needs authenticated screenshots, keep auth policy in the app and use auth.setupScript only to activate that app-owned contract. The module should export a default async function that receives:
browsercontextpagebaseUrlsnapshotDirworktreeDirlogger
Example:
import { setVisualDiffBypassCookie } from "pr-visual-diff/auth";
export default async function setup({ page, baseUrl }) {
await setVisualDiffBypassCookie({
page,
baseUrl,
secret: process.env.VISUAL_DIFF_BYPASS_SECRET
});
}The setup context storage state is reused for subsequent route captures.
If you prefer headers, set capture.headers in .visualdiff.json. For the full bypass contract, fixture-mode guidance, and CI setup, see docs/authenticated-visual-diff.md.
.visual-diff/
report.html
manifest.json
route-slug/
desktop/
before.png
after.png
diff.png
mobile/
before.png
after.png
diff.png
packages/
cli/
core/
git/
runner/
screenshots/
diff-engine/
reporter/
examples/
nextjs/
vite-react/
- Expects the target app to live at the repository root
- Assumes one app per repo for V1
- Install/build/start commands run from the repo root
- Dirty tracked working-tree changes block execution
This repository uses npm workspaces. After installing dependencies:
npm install
npm test