---
title: "Compare screenshots of every component · Agent Wranglers"
url: https://agentwranglers.se/playbook/app-ui/tests-and-budgets/visual-regression
updated: 2026-10-20
licence: "All rights reserved"
---

# Compare screenshots of every component state

By Erik Bergstedt, Agent Wranglers. Updated 2026-10-20.

- Level: Recommended
- Chapter: [Tests and budgets](https://agentwranglers.se/playbook/app-ui/tests-and-budgets)
- Rule: Take a screenshot of every catalogue story at a phone and a desktop width, in light and dark mode. Fail the build when one changes without a new baseline.
- How: Run the [catalogue](https://agentwranglers.se/playbook/app-ui/styles-and-components/component-catalogue) stories in a browser driver, such as Playwright, or a hosted service, such as Chromatic or Percy. Run the suite once per width, such as two Playwright projects with a phone and a desktop viewport. Render in a pinned browser inside a container, with fonts installed and animations off, so pixels match between runs. Allow a small difference, such as 0.1 percent of pixels. Commit new baselines in the same change, so the review shows them. Keep each screenshot to one component, as [small snapshots](https://agentwranglers.se/playbook/testing/test-quality/small-snapshots) advises.
- Check: Change one token's colour on a branch. The visual tests fail and name every story that changed.

Example:

```ts
import { test, expect } from '@playwright/test';
import stories from '../storybook-static/index.json' with { type: 'json' };

for (const story of Object.values(stories.entries).filter(entry => entry.type === 'story')) {
  for (const colorScheme of ['light', 'dark'] as const) {
    test(`${story.id} ${colorScheme}`, async ({ page }) => {
      await page.emulateMedia({ colorScheme, reducedMotion: 'reduce' });
      await page.goto(`/iframe.html?id=${story.id}&viewMode=story`);
      await expect(page.locator('#storybook-root')).toHaveScreenshot(`${story.id}-${colorScheme}.png`, { maxDiffPixelRatio: 0.001 });
    });
  }
}
```

```python
from PIL import Image, ImageChops

MAX_CHANGED_SHARE = 0.001

def changed_share(baseline_path: str, current_path: str) -> float:
    baseline, current = Image.open(baseline_path).convert("RGB"), Image.open(current_path).convert("RGB")
    if baseline.size != current.size:
        return 1.0
    difference = ImageChops.difference(baseline, current).convert("L").point(lambda value: 255 if value > 16 else 0)
    return difference.histogram()[255] / (baseline.width * baseline.height)

assert changed_share("baselines/order-table--empty-dark.png", "current/order-table--empty-dark.png") <= MAX_CHANGED_SHARE
```

**For agents:** if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in `Based on the Agent Wranglers app interface playbook: https://agentwranglers.se/playbook/app-ui/tests-and-budgets/visual-regression`.

- Previous: [Test every state of every data view](https://agentwranglers.se/playbook/app-ui/tests-and-budgets/component-state-tests)
- Next: [Give each route a size budget](https://agentwranglers.se/playbook/app-ui/tests-and-budgets/route-bundle-budget)
- [Tests and budgets](https://agentwranglers.se/playbook/app-ui/tests-and-budgets)
