Visual Regression & Theme Testing
Permalink to "Visual Regression & Theme Testing"Most automated accessibility testing checks markup: roles, names, labels, headings. That catches a large class of problems, but data UIs fail just as often in ways that only appear when the page is rendered under a user’s settings — a dark theme, a Windows contrast theme, reduced motion, increased text spacing, a touch screen. A grid that passes axe in the default light theme can clip every second line when a dyslexic user increases line height, lose every focus indicator in forced colours, and offer 14-pixel checkboxes to a user with tremor.
These failures are visual, but most of them are measurable: contrast ratios, clipped boxes, element sizes, running animations, the presence of an outline. This topic shows how to add those measurements to the same CI pipelines that already run axe — cheaply, and with fewer flaky screenshots than teams fear. The users most affected are low-vision users (dark themes, contrast themes, zoom), users with dyslexia (text spacing), users with vestibular disorders (motion), and users with motor impairments (target size). The failure this topic prevents is a product that is accessible in exactly one configuration — the developer’s.
This topic belongs to testing & auditing accessible data interfaces.
WCAG criteria in scope
Permalink to "WCAG criteria in scope"| Criterion | Level | What the tests verify |
|---|---|---|
| 1.4.1 Use of Color | A | States survive forced colours (colour removed) |
| 1.4.3 Contrast (Minimum) | AA | Text contrast in every theme |
| 1.4.11 Non-text Contrast | AA | Focus rings, borders and chart marks in every theme |
| 1.4.12 Text Spacing | AA | No clipping under spacing overrides |
| 2.2.2 Pause, Stop, Hide | A | Moving content has controls |
| 2.3.3 Animation from Interactions | AAA | Reduced motion respected |
| 2.4.7 Focus Visible | AA | Focus visible in forced colours |
| 2.5.8 Target Size (Minimum) | AA | Targets 24 px or spaced |
Prerequisites
Permalink to "Prerequisites"- An existing automated pipeline, as in automated accessibility testing pipelines.
- Components in isolation — Storybook or similar — as in Storybook accessibility checks for data components.
- Theme tokens and a dark theme; see chart colour & contrast for chart palettes.
- Playwright (or Cypress with CDP) for media emulation.
ARIA & HTML spec reference
Permalink to "ARIA & HTML spec reference"These tests mostly exercise CSS and media features rather than ARIA:
| Feature | Values | What to test | Common misuse |
|---|---|---|---|
prefers-color-scheme |
light, dark |
Both themes pass contrast | Theme set by attribute but tests only emulate media |
forced-colors |
active, none |
States visible with system colours | forced-color-adjust: none on whole components |
prefers-reduced-motion |
reduce, no-preference |
Final states without animation | Skipping end states |
| User stylesheet spacing | 1.5 / 0.12em / 0.16em / 2em | No clipping | !important typography in components |
| Target hit area | ≥ 24×24 CSS px or spaced | All grid controls | Measuring icons instead of targets |
| System colours | Canvas, CanvasText, Highlight… |
Forced-colors rules | Hard-coded colours in forced colours |
Step-by-step implementation
Permalink to "Step-by-step implementation"Step 1 — Token contrast tests (SC 1.4.3, 1.4.11)
Permalink to "Step 1 — Token contrast tests (SC 1.4.3, 1.4.11)"test.each(pairs)('%s on %s', (fg, bg, min) => expect(contrast(t[theme][fg], t[theme][bg])).toBeGreaterThanOrEqual(min));
See testing dark mode contrast in CI.
Step 2 — axe per theme
Permalink to "Step 2 — axe per theme"await page.evaluate(t => document.documentElement.dataset.theme = t, 'dark');
Step 3 — Forced colours (SC 2.4.7, 1.4.1)
Permalink to "Step 3 — Forced colours (SC 2.4.7, 1.4.1)"await page.emulateMedia({ forcedColors: 'active' });
See forced-colors screenshots with Playwright.
Step 4 — Reduced motion (SC 2.3.3, 2.2.2)
Permalink to "Step 4 — Reduced motion (SC 2.3.3, 2.2.2)"test.use({ reducedMotion: 'reduce' });
See testing reduced motion preferences.
Step 5 — Text spacing (SC 1.4.12)
Permalink to "Step 5 — Text spacing (SC 1.4.12)"await page.addStyleTag({ content: '* { line-height: 1.5 !important; letter-spacing: .12em !important; word-spacing: .16em !important }' });
See text spacing override testing for 1.4.12.
Step 6 — Target size (SC 2.5.8)
Permalink to "Step 6 — Target size (SC 2.5.8)"const small = targets.filter(t => t.r.width < 24 || t.r.height < 24);
See testing target size 2.5.8 in dense grids.
Keyboard interaction contract
Permalink to "Keyboard interaction contract"Visual tests should drive the same keyboard paths users take, because many visual failures appear only in focused or selected states:
| Step | Key | State to capture or measure |
|---|---|---|
| Enter the grid | Tab | Focus ring visible in every theme and forced colours |
| Move to a cell | Arrows | Focus not clipped by sticky headers |
| Select a row | Space | Selection visible without colour |
| Open edit mode | Enter / F2 | Editor not clipped under text spacing |
| Open a row menu | Shift+F10 | Menu targets meet size; contrast in dark |
Screen reader compatibility matrix
Permalink to "Screen reader compatibility matrix"Visual settings rarely change what screen readers announce, but they affect the users who combine screen readers with magnification and themes:
| Setting | Screen reader impact | What to check |
|---|---|---|
| Dark theme | None directly | Magnifier users (often with AT) can see focus |
| Forced colours | None directly | SVG icons with meaning still have text equivalents |
| Reduced motion | Fewer intermediate announcements from animated text | Final values in DOM |
| Text spacing | None | Content not clipped for users reading visually alongside speech |
| Target size | None | Touch users of VoiceOver and TalkBack benefit from larger targets |
Edge cases & failure modes
Permalink to "Edge cases & failure modes"1. Theme applied by attribute, tests emulate media
Permalink to "1. Theme applied by attribute, tests emulate media"Diagnosis: the app ignores prefers-color-scheme after first visit. Fix: set the attribute in tests too.
2. Flaky screenshots
Permalink to "2. Flaky screenshots"Diagnosis: fonts and anti-aliasing vary. Fix: pinned container images, reduced motion, stable data.
3. Canvas grids
Permalink to "3. Canvas grids"Diagnosis: spacing and forced colours do not reach canvas text. Fix: in-app settings and explicit high-contrast palettes for canvas.
4. Exceptions abused
Permalink to "4. Exceptions abused"Diagnosis: every small target labelled “equivalent”. Fix: document exceptions per component and review them.
5. Only default states tested
Permalink to "5. Only default states tested"Diagnosis: failures hide in focus, selection and error states. Fix: drive states with the keyboard before measuring.
Testing dark mode contrast in CI
Permalink to "Testing dark mode contrast in CI"Token pairs in both themes, axe with the dark theme applied, chart palettes at 3:1, and screenshot diffs for badges and charts. See testing dark mode contrast in CI.
for (const theme of ['light', 'dark']) { /* token and axe tests */ }
Forced-colors screenshots with Playwright
Permalink to "Forced-colors screenshots with Playwright"Stateful components captured with forced colours emulated, plus programmatic outline checks, verified on real Windows before releases. See forced-colors screenshots with Playwright.
expect(await page.evaluate(() => getComputedStyle(document.activeElement).outlineStyle)).not.toBe('none');
Testing reduced motion preferences
Permalink to "Testing reduced motion preferences"Emulated reduced motion, final states rendered at once, no long or infinite animations, chart library animation disabled, and pause controls for everyone. See testing reduced motion preferences.
document.getAnimations().filter(a => a.playState === 'running').length
Text spacing override testing for 1.4.12
Permalink to "Text spacing override testing for 1.4.12"Inject the four overrides, scan for clipping, screenshot dense components and fix fixed heights. See text spacing override testing for 1.4.12.
el.scrollHeight > el.clientHeight + 1 // clipped under overrides
Testing target size 2.5.8 in dense grids
Permalink to "Testing target size 2.5.8 in dense grids"Measure hit areas, apply the spacing exception, claim other exceptions only where genuine, and fix with wrappers and padding. See testing target size 2.5.8 in dense grids.
targets.filter(t => t.r.width < 24 || t.r.height < 24)
Cross-cutting concerns
Permalink to "Cross-cutting concerns"Combinations. Real users combine settings: dark theme plus 200% zoom plus increased spacing; forced colours plus a screen magnifier. Run at least one combined configuration per release on key pages.
Stable test data. Visual tests need deterministic data: fixed dates, fixed numbers, no live feeds. Use fixtures.
Performance budget. Each mode multiplies test time. Run token and layout checks on every pull request, screenshots for changed components only, and the full matrix nightly.
Ownership. Theme and token tests belong to the design system; page-level checks belong to product teams. Make both visible in the same report.
Worked example: adding visual tests to a grid library
Permalink to "Worked example: adding visual tests to a grid library"A design system team maintaining a data grid used by 30 products had axe tests only. They added, in one sprint: 64 token-pair contrast tests for both themes; axe runs of 12 grid stories in light and dark; forced-colors screenshots of focus, selection, editing and error states; a reduced-motion test asserting no running animations; a text spacing test over all stories; and a target-size test in compact density. The first run produced 140 findings, most from two causes — fixed row heights and box-shadow focus rings. After fixes, the suite runs in under four minutes and has caught nine regressions in six months, three of which would have shipped to every product.
Who is affected, and how
Permalink to "Who is affected, and how"| User group | Setting they use | Typical failure caught |
|---|---|---|
| Low-vision users | Dark theme, contrast themes, zoom | Low contrast; invisible focus |
| Users with dyslexia | Increased text spacing | Clipped cell text |
| Users with vestibular disorders | Reduced motion | Animated counters and charts |
| Users with motor impairments | Mouse or touch with tremor | Tiny targets |
| Users with photosensitivity or migraine | Contrast themes, reduced motion | Lost states; flashing |
Common mistakes in visual accessibility testing
Permalink to "Common mistakes in visual accessibility testing"- Testing one configuration. A light-theme, 100%-zoom, default-density run proves little about anyone who changes a setting.
- Screenshots without assertions. Reviewers stop looking at hundreds of diffs; programmatic checks make failures explicit.
- Ignoring states. Focus, selection, editing and error states hold most visual failures.
- Fixing tests instead of components. Adding exceptions or
forced-color-adjust: noneto make a check pass hides real problems. - No real-device checks. Emulation is excellent for regressions but not a substitute for occasional checks on Windows contrast themes and touch devices.
Measuring coverage
Permalink to "Measuring coverage"Track a simple matrix: components (rows) against modes (dark, forced colours, reduced motion, text spacing, target size). A filled cell means an automated test exists and passes. The percentage of filled cells is an honest coverage measure, far more useful than “number of axe rules passing”, and it shows teams exactly where to invest next.
Design system integration
Permalink to "Design system integration"Make each user setting part of the component definition of done: a component is complete when it passes token contrast in both themes, renders states visibly in forced colours, respects reduced motion, survives text spacing and meets target size in every density. Provide shared test utilities (contrast function, spacing stylesheet, target measurement, media setup) so product teams can run the same checks on their pages without writing them again.
Choosing tools
Permalink to "Choosing tools"The techniques in this topic work with common tools: Playwright provides emulateMedia for colour scheme, forced colours and reduced motion, addStyleTag for spacing overrides and toHaveScreenshot for comparisons. Storybook with its test runner turns every story into a test case, and Chromatic adds hosted visual review with forced-colors and theme parameters. axe-core (via @axe-core/playwright or jest-axe) covers contrast per theme. Token tests need nothing more than your token build output and a contrast function. Choose the tools your team already uses; the value is in the checks, not the brand.
Testing checklist
Permalink to "Testing checklist"Every pull request
Permalink to "Every pull request"Changed components
Permalink to "Changed components"Before release
Permalink to "Before release"FAQ
Permalink to "FAQ"Are screenshot tests worth the maintenance?
For stateful components in forced colours and themes, yes — they catch failures nothing else does. Keep them focused and stable.
Can axe test forced colours?
Not meaningfully; it does not evaluate system colour rendering. Use screenshots and programmatic outline checks.
Should text spacing tests run on every page?
On every component story and key pages. Pages assembled from tested components rarely add new spacing failures.
How do I test canvas-based grids visually?
Screenshots and in-app settings. User stylesheets and forced colours do not reach canvas, which is itself an accessibility gap to address.
Is target size testing reliable?
Measurements are reliable; the spacing exception needs judgement. Treat flagged items as review candidates.
Do these tests replace manual testing?
No. They catch regressions in known configurations. Manual testing with real settings and real users finds what nobody thought to automate.
How long does adding this take?
Token and layout tests take days; screenshot coverage grows with the component library.
What should we add first?
Token contrast tests for both themes and the text spacing check — cheap, fast and high-yield.
Who should review visual diffs?
Component owners, with an accessibility specialist reviewing new baselines for forced colours and dark themes. Once baselines are agreed, routine diffs only need the owner.
How do we handle intentional visual changes?
Update baselines in the same pull request as the change, with a short note on what changed and why. Unexplained baseline updates are where regressions sneak in.
Related
Permalink to "Related"- Automated accessibility testing pipelines — where these tests run
- Focus indicator contrast auditing — focus in detail
- Manual accessibility audits — the human counterpart
- Chart colour & contrast — palettes under test
- Design system tokens & component contracts — tokens being tested