Last updated
Visual Regression Testing
The Image Diff Viewer compares two images and highlights pixel-level differences. Here is how it is used in visual regression testing:
/* Comparison modes */
Side-by-side mode:
[Before Image] | [After Image]
Best for: Obvious layout changes, color changes
Use when: You want to see both versions simultaneously
Overlay mode (adjustable opacity):
[Before at 50% opacity] overlaid on [After at 50% opacity]
Best for: Checking alignment, subtle position shifts
Use when: Elements may have moved slightly
Difference mode:
[Highlighted diff image — only changed pixels shown]
Red pixels: Present in Before, removed in After
Green pixels: Added in After, not in Before
Yellow pixels: Changed color between versions
Best for: Finding exact location of all changes
Slider mode:
[Before ←→ After] with draggable divider
Best for: Before/after comparisons, design reviews
Use when: Showing stakeholders what changed
/* Difference statistics */
Total pixels: 1,920,000 (1920×1000px)
Changed pixels: 4,832
Percentage changed: 0.25%
Average color diff: 12.4 (out of 255)
Status: Minor changes detected
UI Screenshot Comparison
Comparing before and after a CSS change to verify only intended elements changed:
/* Scenario: Updated button color from blue to green */
Before screenshot: homepage-v1.png
After screenshot: homepage-v2.png
/* Expected diff */
Changed regions:
- Primary CTA button: rgb(26,115,232) → rgb(52,168,83)
- Hover state button: rgb(21,94,190) → rgb(41,134,66)
- Active state button: rgb(16,73,148) → rgb(30,100,50)
/* Unexpected diff (regression found!) */
Changed regions:
- Navigation link color: rgb(26,115,232) → rgb(52,168,83)
⚠ Navigation links also changed — unintended side effect!
Root cause: CSS selector was too broad (.btn → a)
/* Diff statistics */
Expected changed pixels: 1,240 (buttons only)
Actual changed pixels: 3,890 (buttons + nav links)
Regression detected: Yes — 2,650 unexpected pixel changes
/* Fix: narrow the CSS selector */
/* Before: */
a { color: #34a853; }
/* After: */
.btn-primary { color: #34a853; }
Design Mockup vs Implementation Comparison
Comparing a Figma design export with a browser screenshot to find discrepancies:
/* Design handoff verification */
Image A: figma-design-export.png (designer's mockup)
Image B: browser-screenshot.png (developer's implementation)
/* Common discrepancies found by diff viewer */
Typography:
Font weight: 600 (design) vs 500 (implementation)
→ Diff shows slightly different text rendering
Fix: font-weight: 600 in CSS
Spacing:
Padding: 24px (design) vs 20px (implementation)
→ Diff shows elements shifted by 4px
Fix: padding: 24px in CSS
Colors:
Background: #F8F9FA (design) vs #F5F5F5 (implementation)
→ Diff shows subtle color difference
Fix: background-color: #F8F9FA
Border radius:
Radius: 12px (design) vs 8px (implementation)
→ Diff shows slightly different corner curves
Fix: border-radius: 12px
/* Diff report */
Total discrepancies: 4
Critical (layout shifts): 1
Minor (color/style): 3
Pixel-perfect match: No
Recommendation: Fix padding issue first (affects layout)
Automated Visual Testing Pipeline
Integrating image comparison into CI/CD for automated visual regression detection:
/* Using Playwright for screenshot capture + comparison */
const { test, expect } = require('@playwright/test');
test('homepage visual regression', async ({ page }) => {
await page.goto('https://example.com');
await page.waitForLoadState('networkidle');
// Compare against stored baseline screenshot
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixels: 100, // allow up to 100 different pixels
threshold: 0.1, // 10% color difference tolerance per pixel
animations: 'disabled' // disable animations for consistent screenshots
});
});
/* Using pixelmatch for custom comparison */
const pixelmatch = require('pixelmatch');
const { PNG } = require('pngjs');
const fs = require('fs');
function compareImages(img1Path, img2Path, diffPath) {
const img1 = PNG.sync.read(fs.readFileSync(img1Path));
const img2 = PNG.sync.read(fs.readFileSync(img2Path));
const { width, height } = img1;
const diff = new PNG({ width, height });
const numDiffPixels = pixelmatch(
img1.data, img2.data, diff.data,
width, height,
{ threshold: 0.1 }
);
fs.writeFileSync(diffPath, PNG.sync.write(diff));
const percentDiff = (numDiffPixels / (width * height) * 100).toFixed(2);
console.log(`Diff: ${numDiffPixels} pixels (${percentDiff}%)`);
return numDiffPixels;
}
// Fail CI if more than 0.1% of pixels changed
const diff = compareImages('baseline.png', 'current.png', 'diff.png');
if (diff / (1920 * 1080) > 0.001) {
process.exit(1); // fail the build
}
Animated GIF Frame Comparison
The viewer compares animated GIFs frame by frame to detect animation regressions:
/* Frame-by-frame comparison */
loading-spinner-v1.gif vs loading-spinner-v2.gif
Frame 1 (0ms): ✓ Identical
Frame 2 (50ms): ✓ Identical
Frame 3 (100ms): ✗ Different — rotation angle changed
Frame 4 (150ms): ✗ Different — rotation angle changed
Frame 5 (200ms): ✓ Identical
...
/* Diff summary */
Total frames: 12
Identical frames: 8 (67%)
Changed frames: 4 (33%)
Issue: Frames 3-6 have incorrect rotation angles
Spinner rotates 45° per frame in v1
Spinner rotates 30° per frame in v2 (slower animation)
/* Common animation regression causes */
- CSS animation duration changed
- Keyframe percentages modified
- Transform values updated
- Frame rate changed in GIF export settings