Forced-Colors Screenshots with Playwright
Permalink to "Forced-Colors Screenshots with Playwright"Windows contrast themes (formerly High Contrast mode) replace author colours with a small set of system colours. Backgrounds become one colour, text another, links and buttons a third. Anything that relied on a background colour — a selected row tint, a red error cell, a coloured status badge, a focus ring drawn with box-shadow — can disappear. Users of contrast themes are a significant group among people with low vision and some with photosensitivity or migraines. Playwright can emulate forced colours, which makes these failures testable in CI.
This guide belongs to visual regression & theme testing.
Spec reference
Permalink to "Spec reference"- CSS
forced-colorsmedia feature —activewhen the user agent enforces a limited palette. System colour keywords (Canvas,CanvasText,Highlight,HighlightText,ButtonText,LinkText,GrayText) give access to the user’s palette. forced-color-adjust—noneopts an element out of forced colours (use sparingly, e.g. for colour swatches).- SC 1.4.11 Non-text Contrast (AA) and SC 2.4.7 Focus Visible (AA) — focus indicators and UI states must remain perceivable.
- SC 1.4.1 Use of Color (A) — state conveyed only by background colour vanishes in forced colours, which is a useful test of colour independence.
When to use — and when not to
Permalink to "When to use — and when not to"Capture forced-colours screenshots for components with rich state: grids (focus, selection, editing, errors), charts, badges, toggles and custom checkboxes. Add them to visual regression suites that already exist.
Do not treat screenshots as the only check; pair them with programmatic assertions (is an outline present on the focused cell?) so failures are explicit. Do not “fix” forced colours by setting forced-color-adjust: none broadly — that overrides the user’s chosen palette.
The misapplication: a single forced-colours screenshot of a page in its default state. The failures live in states — focus, selection, error — so capture those.
Annotated code example
Permalink to "Annotated code example"// forced-colors.spec.js
import { test, expect } from '@playwright/test';
test.describe('forced colors', () => {
test.use({ colorScheme: 'dark' }); // also run with 'light'
test.beforeEach(async ({ page }) => { await page.emulateMedia({ forcedColors: 'active' }); });
test('grid states', async ({ page }) => {
await page.goto('/storybook/iframe.html?id=grid--with-selection');
const grid = page.getByRole('grid', { name: 'Invoices' });
await expect(grid).toHaveScreenshot('grid-default-fc.png');
await page.keyboard.press('Tab'); // focus first cell
await expect(grid).toHaveScreenshot('grid-focused-fc.png');
// Programmatic check: focused cell has a visible outline (SC 2.4.7)
const outline = await page.evaluate(() => getComputedStyle(document.activeElement).outlineStyle);
expect(outline).not.toBe('none');
await page.keyboard.press('Space'); // select row
await expect(grid).toHaveScreenshot('grid-selected-fc.png');
});
});
/* Styles the screenshots should confirm */
.grid [aria-selected="true"] { outline: 2px solid transparent; } /* becomes visible in forced colours */
@media (forced-colors: active) {
.grid [aria-selected="true"] { outline-color: Highlight; background: Highlight; color: HighlightText; }
.grid :focus-visible { outline: 3px solid CanvasText; }
.badge { border: 1px solid CanvasText; }
}
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"Forced colours change visuals, not semantics; screen readers are unaffected. The users are mostly sighted, often keyboard users with low vision.
| Check in forced colours | Pass condition | Typical failure |
|---|---|---|
| Focused grid cell | Outline visible in system colour | Focus via box-shadow — invisible |
| Selected row | Highlight background or outline | Tint removed — no selection visible |
| Error cell | Text and icon visible | Red fill removed — no error visible |
| Chart series | Distinguishable by pattern or label | All series one colour |
| Custom checkbox | Box and tick visible | Background-image tick removed |
Integration context
Permalink to "Integration context"Manual forced-colours audits are described in Windows High Contrast audits for data UIs; this guide automates the repeatable part. Charts in forced colours are covered in forced colors mode for SVG charts. Screenshot automation more broadly is in automating focus indicator screenshots with Playwright.
Gotchas
Permalink to "Gotchas"Emulation is Chromium-only. Playwright’s forcedColors emulation works in Chromium; the real feature is Windows-specific, and Firefox implements its own variant. Use emulation for regression tests and verify releases on Windows manually.
Baseline churn. Fonts and anti-aliasing differ between machines; run screenshots in a pinned container image.
SVG fills. SVG fill values are forced too in many cases; charts that rely on fill colours alone become monochrome. Patterns and labels survive.
Common mistakes
Permalink to "Common mistakes"- Capturing only default states.
- Using
forced-color-adjust: noneon whole components. - Focus indicators drawn with
box-shadow. - No programmatic assertions alongside screenshots.
Framework notes
Permalink to "Framework notes"Storybook stories are ideal targets: one story per state, captured with forced colours emulated via the test runner. Chromatic supports forced colours through its forcedColors parameter in recent versions. In Cypress, use the Chrome DevTools Protocol (Emulation.setEmulatedMedia with forced-colors: active) via cy.task or a plugin.
// Storybook parameter (Chromatic) for a forced-colors snapshot
export const Selected = { parameters: { chromatic: { forcedColors: 'active' } } };
Worked example
Permalink to "Worked example"A data grid component used background tints for selection and box-shadow for focus. In forced colours, users of Windows contrast themes could not see either. After adding forced-colors stories for focus, selection, editing and error states, the team moved focus to outline, added a forced-colors rule mapping selection to Highlight, and gave error cells an icon with text. The new screenshots became the baselines; two later regressions — a refactor back to box-shadow and a new badge without a border — were caught in pull requests.
Testing checklist
Permalink to "Testing checklist"Design system notes
Permalink to "Design system notes"Add a forced-colors section to each component’s stylesheet and a forced-colors story set to each component’s documentation. Make “renders correctly in forced colours” part of the component definition of done.
FAQ
Permalink to "FAQ"Is forced colours the same as dark mode?
No. Dark mode is your palette; forced colours is the user’s palette, applied by the browser.
Can I test forced colours on macOS or Linux?
Emulation in Chromium works on any OS. The real Windows feature should still be checked before releases.
Should I opt components out with forced-color-adjust: none?
Only small parts that must show real colours, like colour swatches in a palette picker. Never whole components.
Which system colours should I use?
CanvasText for text and outlines, Canvas for backgrounds, Highlight and HighlightText for selection, LinkText for links, GrayText for disabled.
Does forced colours affect SVG charts?
Yes; fills and strokes may be forced. Use patterns, dashes and labels so series remain distinguishable.
How many screenshots are enough?
One per meaningful state for each stateful component. That is usually a few dozen for a data UI library.
Should icons have forced-colors styles?
SVG icons drawn with currentColor adapt automatically. Icons with hard-coded fills may vanish or become invisible; switch them to currentColor or add forced-colors rules.
Related
Permalink to "Related"- Windows High Contrast audits — the manual audit
- Forced colors mode for SVG charts — charts specifically
- Testing dark mode contrast in CI — the other theme