PRACTICAL GUIDE / Playwright color scheme contrast media testing
Test dark mode and increased contrast as separate browser preferences
Use Playwright media emulation to catch dark-theme, contrast, and forced-color regressions, with runtime checks that explain visual failures.
In this guide6 sections
What you will learn
- Model three preferences instead of one dark-mode flag
- Test initial load and live preference changes
- Add contrast checks that can catch a real regression
- Diagnose the media state before updating snapshots
The dark-theme screenshot is correct, but the focus ring disappears when a user asks for more contrast. Another test passes because it sets colorScheme: 'dark' and assumes that covers every color preference. It does not: the browser exposes dark scheme, increased contrast, and forced colors through different media features.
A useful suite keeps those signals separate and checks the product result behind each one. It also proves whether the page responds only at startup or continues responding when the operating-system preference changes while the tab is open.
Model three preferences instead of one dark-mode flag
prefers-color-scheme tells content whether the user prefers a light or dark color scheme. Playwright can set it through the colorScheme context option or page.emulateMedia({ colorScheme }). The page then observes the value through CSS media queries or window.matchMedia().
prefers-contrast carries a contrast preference. Playwright's contrast option supports no-preference and more; page-level contrast emulation was added in Playwright 1.51. The CSS media feature itself has other values, but naming those in CSS does not mean Playwright accepts them as API arguments. Tests should stay inside the documented Playwright union rather than inventing less or custom options.
forced-colors reports whether the user agent is enforcing a limited color palette. Playwright exposes forcedColors with active and none. This is not a stronger spelling of contrast: 'more'. Under forced colors, browsers can replace author colors at paint time, remove effects such as box shadows and text shadows, and use system colors for common controls. A component that relies on a shadow as its only boundary can become hard to identify even if the application's dark palette is excellent.
Keep a fourth concept out of this trio: your application's manual theme setting. A user may select Light inside the product while the operating system prefers Dark. Product policy decides which wins. The test must express that policy explicitly instead of assuming system preference always has priority.
For a system-following page, the initial result should agree with matchMedia('(prefers-color-scheme: dark)'). For a manual override, the browser signal may be dark while the rendered product remains light by design. The oracle is not "DOM theme equals media query" in every case. It is "the documented precedence rule produces the visible theme."
Media type is another independent setting. page.emulateMedia({ media: 'print' }) changes screen-versus-print media behavior. It does not enable dark mode or contrast automatically. Avoid combining print, color scheme, reduced motion, and forced colors in one unexplained project. When a snapshot fails, you need to know which input was intended to change it.
The browser's media signal is only the mechanism. CSS-first applications may react without JavaScript. Other applications subscribe to MediaQueryList changes and write a class or data attribute. Both are valid designs. Assertions should observe the browser state and one user-facing consequence, not require an internal class merely because the current framework uses it.
For CSS-only behavior, computed style or a screenshot supplies the product consequence. For JavaScript-managed themes, an attribute may be a stable public hook if the application deliberately exposes resolved theme state. Accessible labels, visible icons, and chart styles can add stronger evidence. Choose signals that would change when a real theme regression occurs.
Native form controls create a related boundary. A page can switch its custom background to dark while inputs, scrollbars, and built-in control surfaces retain a light rendering because the document did not communicate its supported schemes. Conversely, declaring scheme support does not automatically fix custom component colors. Include at least one real input, select, dialog, or scrollable region on the visual surface. A screenshot of decorative cards alone can miss the seam between author styling and user-agent styling.
Images need an explicit policy too. A logo may have separate light and dark assets, an illustration may remain unchanged, and a photograph should usually not be inverted by a blanket filter. Assert the chosen asset through its accessible purpose and, where stable, its URL or rendered screenshot. Do not infer that every bright image in a dark screenshot is a regression. The product's content design decides whether it adapts.
Third-party content can stop at an iframe boundary. Emulating the top-level page does not guarantee that an embedded vendor document implements your theme protocol, even though browser media preferences may be visible to documents according to their own environment. A product that sends an explicit theme parameter to a chart or payment widget needs a contract test at that integration boundary. A product with no control over the embedded surface needs a documented exception and a visual review, not a locator aimed through an inaccessible cross-origin frame.
Preference combinations need named expectations. Dark plus more contrast is not necessarily "darker." A good increased-contrast design may lighten borders, remove subtle transparency, or use a brighter focus indicator. Forced colors may remove the brand background entirely. Review each combination against information and operability, not against the aesthetic direction implied by its project name.
Test initial load and live preference changes
Initial-load coverage catches server rendering, hydration, and first-paint decisions. Live-change coverage catches missing or incorrectly removed media-query listeners. They are different failures. A page can render dark correctly when the context starts dark and still ignore a change made after navigation.
The example product follows the system until a user chooses a manual override. It exposes data-resolved-theme on the root element as a supported diagnostic hook and labels the theme selector. The first test begins light, loads the page, then changes the same page to dark. It verifies the browser signal before checking the application result.
// tests/media/theme-preference.spec.ts
import { test, expect } from '@playwright/test';
test('follows a live system color-scheme change', async ({ page }) => {
await page.emulateMedia({ colorScheme: 'light' });
await page.goto('/account/appearance');
await expect(page.locator('html')).toHaveAttribute(
'data-resolved-theme',
'light',
);
await page.emulateMedia({ colorScheme: 'dark' });
await expect.poll(() => page.evaluate(() => ({
dark: matchMedia('(prefers-color-scheme: dark)').matches,
light: matchMedia('(prefers-color-scheme: light)').matches,
}))).toEqual({ dark: true, light: false });
await expect(page.locator('html')).toHaveAttribute(
'data-resolved-theme',
'dark',
);
await expect(page.getByRole('img', { name: 'Dark theme preview' }))
.toBeVisible();
});
test('keeps a manual light override when the system turns dark', async ({
page,
}) => {
await page.emulateMedia({ colorScheme: 'light' });
await page.goto('/account/appearance');
await page.getByLabel('Theme').selectOption('light');
await expect(page.getByRole('status')).toHaveText('Theme saved');
await page.emulateMedia({ colorScheme: 'dark' });
expect(await page.evaluate(() =>
matchMedia('(prefers-color-scheme: dark)').matches,
)).toBe(true);
await expect(page.locator('html')).toHaveAttribute(
'data-resolved-theme',
'light',
);
await page.reload();
await expect(page.getByLabel('Theme')).toHaveValue('light');
await expect(page.locator('html')).toHaveAttribute(
'data-resolved-theme',
'light',
);
});The first case fails if the page never registered a change listener, registered it against the wrong query, or updates its preview without updating resolved theme. The second fails if system changes incorrectly override a saved user choice or if the choice is not durable across navigation. They do not restate the same dark-mode check with different variable names.
Await page.emulateMedia(). Calling it without await creates a race between browser state and the next assertion. Do not repair that race with a sleep. First assert matchMedia so a failure tells you whether emulation was applied. Then let Playwright's locator assertion wait for the application response.
If the application intentionally reads the preference only at startup, make that a documented product choice before weakening the live test. Users can change system appearance while a long-lived application remains open. A full reload requirement may be acceptable for a static document and frustrating for an operations console. The suite should enforce the chosen behavior, not whatever the first implementation happened to do.
Theme hydration can produce a flash that the final DOM misses. A server-rendered light shell may switch to dark after client JavaScript runs. A final screenshot can pass even though users see a bright frame. Detecting that flash requires an earlier observation strategy, such as server-rendered theme hints, controlled filmstrip review, or a product performance marker. Do not invent a millisecond threshold without measuring a requirement. Treat flash detection as a separate test because it has a different timing oracle from final theme correctness.
A practical server-rendering case starts the context in dark mode and requests a page without any saved override. Inspect the first response document or the earliest trace snapshot for the theme hint the server promises, then assert the hydrated result remains dark. If the server cannot know client preference, the product may instead promise an inline bootstrap that resolves theme before visible content. Test that implementation at its real boundary. Waiting for networkidle and taking a final screenshot cannot reveal whether a light shell flashed earlier.
The opposite near-miss occurs when tests seed a manual override in local storage before navigation. The seed prevents the flash and makes every dark case pass, but it no longer tests system preference. Keep at least one clean-profile System case and label seeded Light or Dark cases as manual-choice coverage. Shared storage state between cases makes these categories impossible to trust, so use Playwright's isolated contexts or clear only the application-owned key through deliberate setup.
Opening a second tab provides another useful live-change example. Page-level emulateMedia() changes the page on which it is called. A test that expects every page in the context to change because one tab changed has asserted a harness behavior it did not configure. Use context or project options for a preference that must apply when each page is created, and page-level calls for a controlled transition in one document. Then verify the product's own cross-tab theme synchronization separately if it stores manual overrides.
That cross-tab case can expose a genuine defect: tab A saves Manual Dark, tab B remains System Light, and both show "Dark" selected after reload. The system signal is not the cause. The product failed to distribute or re-read its manual setting. Observe the selector value, resolved theme, and storage-driven event sequence without changing media midway. Keeping that test separate stops a storage bug from being misclassified as matchMedia handling.
Add contrast checks that can catch a real regression
An assertion that only asks whether matchMedia('(prefers-contrast: more)') is true mostly tests the harness. Pair it with an element whose presentation is supposed to change. The example design increases focus outline width and changes a dashed boundary to solid when more contrast is requested. Those are stable product rules, unlike sampling an arbitrary antialiased pixel.
// tests/media/contrast-preference.spec.ts
import { test, expect } from '@playwright/test';
test('strengthens the focused account control under more contrast', async ({
page,
}) => {
await page.emulateMedia({
colorScheme: 'dark',
contrast: 'no-preference',
forcedColors: 'none',
});
await page.goto('/account/security');
const control = page.getByRole('button', { name: 'Add security key' });
await control.focus();
const normalOutline = await control.evaluate(element => {
const style = getComputedStyle(element);
return { style: style.outlineStyle, width: style.outlineWidth };
});
expect(normalOutline).toEqual({ style: 'dashed', width: '2px' });
await page.emulateMedia({ contrast: 'more' });
expect(await page.evaluate(() =>
matchMedia('(prefers-contrast: more)').matches,
)).toBe(true);
await expect.poll(() => control.evaluate(element => {
const style = getComputedStyle(element);
return { style: style.outlineStyle, width: style.outlineWidth };
})).toEqual({ style: 'solid', width: '3px' });
await expect(page).toHaveScreenshot('security-dark-more-contrast.png');
});The pixel values in that code are declared design tokens for the example product, not measurements from an unrun experiment. A real repository should import or document the approved values so a design change updates the test through review. The screenshot provides broader regression coverage, while computed style makes the intended focus change obvious when the image diff is noisy.
A second contrast failure often hides in charts. The default chart distinguishes series by color, while increased contrast adds line patterns, markers, or labels. A screenshot can protect the rendered result, but also assert that the legend exposes unambiguous series names and that data remains reachable without color alone. Do not claim the screenshot measures contrast ratio. It can reveal an accidental return to the old chart, not certify every foreground-background pair.
Work that chart example through the user task. Begin under contrast: 'no-preference' and confirm the Sales and Refunds series are named in the legend. Change the page to contrast: 'more'. The media query must match, the chart wrapper should expose the product's enhanced-contrast state, and each plotted series should gain its documented non-color cue, such as a marker shape or dash pattern. Finally, focus the legend controls and toggle a series to prove the adaptation did not place an overlay over interactions.
The oracle should come from the rendered chart contract, not from a hard-coded configuration object passed into the same component. If the test reads series[0].dash = 'solid' from a fixture and asserts that fixture still says solid, no product change can make it fail. Inspect SVG attributes, canvas-adjacent accessible summaries, or user-visible legend state after rendering. Canvas pixels alone can be hard to diagnose, so a product that ships a canvas chart should provide accessible data and stable controls regardless of the visual test.
Form validation supplies a third contrast scenario. Many designs use a pale red border and color-only helper text for errors. Increased contrast may require a thicker boundary, an icon with an accessible name, or an explicit "Error" prefix. Submit an invalid form after enabling more contrast and assert the same validation message, focus movement, and enhanced boundary rule. This verifies that preference CSS did not accidentally hide the error while strengthening its presentation.
That form case also guards against an attractive but harmful shortcut: removing backgrounds and borders wholesale under high contrast. Simplification can erase selected tabs, invalid fields, and disabled states. Test at least one example of each state the design communicates visually. The suite does not need every control permutation in the browser, but it needs representative states where loss of a boundary changes what the user can understand.
Forced colors needs its own case because author-level computed colors may not describe final paint. MDN documents that the user agent can force several color properties and remove non-URL background images, shadows, and other effects. A test that expects the normal hex value under forcedColors: 'active' is asserting against the mode's purpose.
Use system-aware outcomes instead. Focus the primary controls and ensure a visible boundary remains. Check that icons conveying state have text or an accessible name. Take a forced-colors snapshot in a stable browser project if visual review is part of your baseline process. The exact palette can depend on the browser and platform, so keep baselines scoped accordingly.
// tests/media/forced-colors.spec.ts
import { test, expect } from '@playwright/test';
test('keeps status and focus visible with forced colors active', async ({
page,
}) => {
await page.emulateMedia({ forcedColors: 'active' });
await page.goto('/jobs/42');
expect(await page.evaluate(() =>
matchMedia('(forced-colors: active)').matches,
)).toBe(true);
await expect(page.getByRole('status')).toHaveText('Deployment paused');
const resume = page.getByRole('button', { name: 'Resume deployment' });
await resume.focus();
await expect(resume).toBeFocused();
await expect(page).toHaveScreenshot('deployment-forced-colors.png');
});Focus ownership alone does not prove a visible focus indicator, which is why the visual assertion remains. Conversely, the screenshot alone can be difficult to diagnose. The role, text, and focus assertions say whether the semantic control survived while the image shows how the browser painted it.
Diagnose the media state before updating snapshots
A dark screenshot diff does not automatically mean the baseline is stale. First determine whether Playwright applied the requested signal. Then determine whether the application resolved the intended theme. Finally inspect the affected computed styles and image.
Attach a small state record at the point of failure. It should include the exact media queries used by the product, the manual theme value if one exists, and a few stable computed properties from the broken component. Avoid dumping the whole DOM or every CSS declaration. More data can make the relevant disagreement harder to see.
// tests/media/attach-media-state.ts
import type { Page, TestInfo } from '@playwright/test';
export async function attachMediaState(page: Page, testInfo: TestInfo) {
const state = await page.evaluate(() => {
const target = document.querySelector<HTMLElement>('[data-testid="panel"]');
const style = target ? getComputedStyle(target) : null;
return {
queries: {
dark: matchMedia('(prefers-color-scheme: dark)').matches,
moreContrast: matchMedia('(prefers-contrast: more)').matches,
forcedColors: matchMedia('(forced-colors: active)').matches,
print: matchMedia('print').matches,
},
resolvedTheme: document.documentElement.dataset.resolvedTheme ?? null,
panel: style ? {
color: style.color,
backgroundColor: style.backgroundColor,
outlineStyle: style.outlineStyle,
outlineWidth: style.outlineWidth,
} : null,
};
});
await testInfo.attach('media-state.json', {
body: Buffer.from(JSON.stringify(state, null, 2)),
contentType: 'application/json',
});
}If dark is false, inspect the test setup and whether another helper reset emulation. If it is true while resolvedTheme remains light in a system-following case, the browser did its job and application preference handling did not. If both agree but a panel retains light colors, inspect selector specificity, CSS variables, shadow DOM boundaries, or a component that cached tokens outside the theme update.
If computed styles look correct but the screenshot differs, check fonts, animations, caret state, image assets, viewport, device scale factor, and operating-system rendering. Those are visual-test controls, not reasons to approve a color change automatically. Read the actual and expected images side by side. A broad diff over every surface suggests theme input or baseline environment; one unchanged control inside an otherwise correct dark page suggests a component-level token defect.
Trace Viewer helps establish sequence. Its action list can show when emulateMedia ran relative to navigation and interaction, and DOM snapshots show document state around later actions. The trace does not automatically explain the result of every matchMedia call or certify computed paint. The explicit JSON attachment fills that gap.
One near-miss comes from a stored manual override. The test requests dark, matchMedia is true, and the page remains light. That is a bug only if the scenario begins in system-following mode. Inspect storage and the visible selector before changing application code. Better yet, make each test choose System, Light, or Dark through owned setup so the starting policy is not inherited from another case.
Another near-miss comes from forced colors. A screenshot may lose shadows and background decorations exactly because the browser is enforcing the mode. Updating author colors to fight those changes can reduce usability. Check whether the missing effect carried information. Replace an information-bearing shadow with a border or text cue under forced colors; leave purely decorative loss alone.
Focus is the classic forced-color failure because modern components often use box-shadow as a ring. MDN documents that forced colors sets box shadow to none. If the component has no outline or border fallback, Playwright can focus it successfully while the screenshot shows no focus treatment. Diagnose this by recording focus ownership, computed outline style and width, and the forced-colors media result. The product fix belongs in its forced-color CSS, not in a longer focus timeout.
Background images can fail in a similar way when they are non-URL gradients used to convey selected or warning state. Under forced colors, such decoration may disappear. Preserve meaning with text, native state, borders, or system colors. Do not add a screenshot tolerance large enough to ignore the missing state. A tolerance changes comparison sensitivity; it does not restore information for a user.
When only one CI runner shows a diff, compare browser engine and operating-system image before blaming the media query. Forced-color paint and font rendering can be platform-sensitive. The semantic assertions should remain stable across approved platforms, while visual baselines may need to be generated and reviewed per environment. Mixing baselines from developer macOS with Linux CI creates work that preference logic cannot resolve.
Build a CI matrix without multiplying every test
Running every end-to-end test under light, dark, more contrast, and forced colors multiplies browser launches, screenshots, and maintenance. Most checkout or API workflows do not gain four times the coverage. Create a focused media-preference spec set and give each project one explicit state.
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/media',
use: {
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'theme-light',
use: {
...devices['Desktop Chrome'],
colorScheme: 'light',
contextOptions: { contrast: 'no-preference', forcedColors: 'none' },
},
},
{
name: 'theme-dark',
use: {
...devices['Desktop Chrome'],
colorScheme: 'dark',
contextOptions: { contrast: 'no-preference', forcedColors: 'none' },
},
},
{
name: 'contrast-more',
use: {
...devices['Desktop Chrome'],
colorScheme: 'dark',
contextOptions: { contrast: 'more', forcedColors: 'none' },
},
},
{
name: 'forced-colors',
use: {
...devices['Desktop Chrome'],
colorScheme: 'light',
contextOptions: { contrast: 'no-preference', forcedColors: 'active' },
},
},
],
});Note where contrast and forcedColors are declared, because this is the trap that quietly ruins a preference matrix. colorScheme is a first-class Playwright Test use option. contrast, forcedColors, and reducedMotion are not. They are browser context options, so in a config they have to travel through use: { contextOptions: { ... } }, which is the form Playwright's own documentation shows. The PlaywrightTestOptions interface in the installed playwright/types/test.d.ts lists colorScheme and contextOptions and contains no contrast or forcedColors member at all.
Writing them as bare keys under use is easy to reach for because page.emulateMedia() accepts exactly those names as direct arguments. The method signature and the project option are different surfaces with different shapes, and only one of them is checked for you by default.
The failure is silent in the way that costs the most. TypeScript will reject the bare form under excess property checking, reporting that contrast does not exist in the UseOptions type, but a repository running JavaScript configuration, loose type settings, or a suppressed error keeps going. At runtime the unknown key is simply ignored. Probe a project written that way and the contrast-more project reports prefers-contrast: more as false while the forced-colors project reports forced-colors: active as false. Both projects run in an ordinary palette under names that promise otherwise.
That is worse than a missing test. The forced-colors project still executes toHaveScreenshot('deployment-forced-colors.png'), so the first run writes a normal-palette image and files it as the forced-colors baseline. Every later run compares forced-colors-off against forced-colors-off and passes. The suite reports green coverage for a mode it never entered, and a reviewer approving that baseline has no visual cue that anything is wrong, because a normal screenshot of a working page looks like a working page.
Guard it with the same probe used elsewhere in this article. Add one cheap test per media project that asserts the media queries the project name claims, and let it run before the visual cases. A project called contrast-more whose probe reports moreContrast: false should fail on the probe, not silently bless an image. Keeping the type check strict on the config file catches the same mistake earlier, at the cost of nothing.
Project defaults cover initial load. Keep one or two page-level emulateMedia cases for live changes; duplicating those under all four projects adds combinations without a clear claim. Tests should either consume the project's fixed preference or control the page transition themselves, not ambiguously depend on both.
This matrix uses one browser so visual baselines remain manageable. If the product supports media preferences across Chromium, Firefox, and WebKit, add a small semantic smoke set for each engine before cloning every screenshot. Browser-specific paint can require separate baselines. That storage and review burden is part of the coverage cost.
Roll out by first adding match-state attachments to existing dark tests. Next, split contrast and forced-color cases from dark mode and run them non-blocking until the expected product policy is settled. Approve visual baselines only after semantic assertions pass. Finally, make a small set of critical surfaces required: navigation, forms, focus states, error messages, charts, and any custom control whose boundary depends on color or shadow.
An existing suite usually has hidden assumptions in shared helpers. Search for context creation that sets colorScheme, page helpers that call emulateMedia, and storage seeds that select a manual theme. Record which layer owns each case. During migration, fail when a test tries to use a fixed-theme project and then silently changes that preference through a global hook. One owner per test keeps the media-state attachment interpretable.
Baseline naming should include the relevant project automatically through Playwright's snapshot path behavior or an explicit project token in the repository convention. Reusing dashboard.png for light, dark, and forced-color results invites overwrites or confusing update churn. Do not solve collisions by writing snapshots to a shared ad hoc directory. Let Playwright's project-aware snapshot system own them.
Run the semantic checks on every pull request and choose visual frequency according to repository cost. If four projects across ten pages produce forty images, reviewers need time and stable infrastructure to inspect them. A nightly job can cover secondary surfaces, but critical focus and error states should remain close to the changes that break them. State that release rule in CI rather than calling every preference snapshot equally critical.
The matrix also creates maintenance when a design token changes. One intended focus-ring update may touch light, dark, and more-contrast baselines while forced colors should remain unchanged. That asymmetry is useful evidence. If every image changes identically, inspect whether a global environment factor such as font or viewport moved instead of approving the batch.
Snapshot updates deserve the same review as product code. A bulk update can bless a lost focus ring across every project. Require the change owner to name the intended token or layout change and inspect the more-contrast and forced-color images separately. Their differences may be expected for entirely different reasons.
Know what emulation cannot certify
Do not claim that colorScheme: 'dark' tests increased contrast. Set contrast: 'more' and assert an outcome designed for that preference. The inputs are independent even when the design chooses overlapping tokens.
Do not claim that contrast: 'more' proves a WCAG contrast ratio. Emulation makes the media query match. It does not calculate every foreground-background pair, account for transparency stacks, or judge text size. Use a suitable accessibility analysis and manual review for that claim.
Do not treat forced colors as a dark theme. The user agent may replace author colors and effects using a limited system palette. Assertions against normal hex values are especially misleading there. Protect information, control boundaries, readable text, and focus visibility.
Do not update snapshots before checking matchMedia and resolved product state. A baseline update can convert a configuration bug into the new expected image. Keep the media-state attachment beside every unexplained visual diff.
Do not run the entire regression suite through every preference merely because projects make it easy. More combinations increase time and review noise. Choose surfaces where theme or contrast can change behavior or meaning, then use component and token tests for the larger combinatorial space.
Do not use an internal theme class as the only oracle unless it is an intentional contract. A class can change while CSS remains broken, or CSS can respond directly without the class. Pair browser state with something a user sees or operates.
Media emulation is strongest when it answers a narrow question: did the browser expose the intended preference, and did the product preserve the right information and interaction under that preference? Keep accessibility conformance, visual approval, and manual-theme precedence as named companion claims. One green screenshot should never be asked to prove all four.
// FIELD DISPATCH
Get the QA Field Notes
Weekly QA battles, AI testing guides, and interview drills. Free on Substack.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Official playwright.dev reference
playwright.dev
Primary documentation selected and verified for the claims in this guide.
- 02Official playwright.dev reference
playwright.dev
Primary documentation selected and verified for the claims in this guide.
- 03Official playwright.dev reference
playwright.dev
Primary documentation selected and verified for the claims in this guide.
- 04Official playwright.dev reference
playwright.dev
Primary documentation selected and verified for the claims in this guide.
FAQ / QUICK ANSWERS
Questions testers ask
Does Playwright dark mode also enable increased contrast?
No. colorScheme controls prefers-color-scheme, while contrast controls prefers-contrast. Set and assert them independently so one preference cannot accidentally stand in for the other.
How do I test a theme change after the page has loaded?
Load the page under one explicit preference, call page.emulateMedia with the new value, and wait for both matchMedia and the user-visible theme result. That sequence catches applications that read the preference once but never subscribe to changes.
Why does matchMedia say dark while the page still looks light?
The browser emulation worked, but the application or CSS did not apply the resulting state. Inspect theme overrides, listener registration, selector scope, and computed styles before changing the screenshot baseline.
Is prefers-contrast the same as forced-colors?
They are different media features. Increased contrast asks content to provide more contrast, while forced colors lets the user agent enforce a limited palette and alter how several author colors and effects are painted.
Can a passing dark-mode screenshot prove WCAG color contrast?
A screenshot comparison only proves similarity to an approved image within its tolerance. Use a contrast-analysis method with known foreground and background relationships for the accessibility claim, and keep media emulation focused on preference handling.
RELATED GUIDES
Continue the learning route
GUIDE 01
Test Reduced Motion with Playwright
Use Playwright reduced motion testing with media emulation to verify static alternatives, disabled animations, usable content, and regression checks in CI.
GUIDE 02
Test localStorage Directly with the Playwright API
Learn Playwright localStorage API testing through practical setup, failure analysis, CI evidence, security boundaries, and measurable release gates for QA and SDET teams.
GUIDE 03
Test sessionStorage Directly with the Playwright API
A practical guide to Playwright sessionStorage API testing, with implementation examples, debugging workflows, CI evidence, security controls, and release gates.
GUIDE 04
Test WebAuthn Passkey Registration with Playwright
Master Playwright WebAuthn passkey registration testing with implementation examples, failure analysis, evidence design, CI controls, and release-ready QA checklists.
GUIDE 05
Test WebSocket Subprotocol Negotiation with Playwright
Learn Playwright WebSocket subprotocol testing with working code, failure cases, debugging steps, and CI evidence for reliable QA automation.