Hero image for The theme the agent never sees — dark mode is invisible to a default-emulation audit (2026)

The theme the agent never sees — dark mode is invisible to a default-emulation audit (2026)

A test: hypothesis, method, results, conclusion — design-agent.dev, 2026-09-29.

The agent’s three representations are theme-blind by default

A design agent perceives a page through three at-rest representations only — the serialized DOM, the accessibility tree, and computed styles — and all three resolve @media (prefers-color-scheme: dark) against an emulated media state whose Playwright colorScheme context option defaults to light.

getComputedStyle, reachable through CDP CSS.getComputedStyleForNode or Playwright’s element.evaluate, returns resolved values, not source text: a declaration inside a media block whose feature evaluates false contributes nothing at all — no custom property, no color, no rule. MDN’s prefers-color-scheme reference is explicit that light and dark are evaluated at computed-style time, and the media-feature overview describes media features as a runtime condition, not a static filter over a stylesheet. So theme is state, and state is opt-in: an agent auditing with default emulation reads a light page and has no signal that a second palette exists.

A site’s dark theme can be complete, accessible and shippable while remaining completely absent from every representation an agent reads by default. Dark tokens inside a media block exist in the stylesheet but not in the DOM, not in the accessibility tree, and not in computed styles until the emulated feature flips. The failure is invisible: the audit does not error, it does not warn, it simply reports a light page as the page. Our static fetch of server-sent HTML and CSS on 2026-09-29 — no render, no JavaScript, no pointer input — found 13 of 20 usable sites (65%) shipping some dark-theme channel in CSS and 7 of 20 (35%) shipping none. Of those 13, six are hook-only, seven expose both channels, and zero are media-only at token level. That last number is the finding this post was built to test, and it cuts against the hypothesis. A dark-first audience still exists for real reasons, and the tokens for it are usually present — just gated behind a state the agent never sets.

Method: a static fetch that never runs JavaScript

The measurement is a static fetch of server-sent HTML plus inline style and up to 12 linked stylesheets per site, with a brace-walk over the CSS that splits declaration blocks by dark channel — and no render, no JavaScript, no pointer input.

The brace-walk separates blocks whose selector context contains a dark media feature from blocks whose selector context contains a DOM dark hook such as [data-theme=dark], [data-color-mode=dark], [data-mode=dark], .dark, or html.dark. Twenty sites were usable; two were excluded — adobe.com on a read timeout, and w3.org/WAI returning 403 to datacenter IPs, which is why the WCAG citations below point at the w3c.github.io mirror instead. Every count quoted in this post comes from that static fetch of server-sent HTML and CSS on 2026-09-29 — no render, no JavaScript, no pointer input — and is reproducible from the published measurement script in the arena. Because no script executed, hook strings that exist only inside inline JavaScript are invisible to the parser, which makes every hook count a lower bound.

Ten sites, three channel classes

The ten sites in the comparison table fall into three channel classes — none, hook-only, and both — as classified by the static CSS brace-walk of 2026-09-29, with github.com at one extreme and linear.app at the other, across a 20-site sample of which 13 ship some dark channel.

Site Dark channel Media-gated dark custom props Hook-gated dark custom props color-scheme: declarations Dark at rest?
design-agent.dev none 0 0 0 No
astro.build none 0 0 1 Yes — literal <html> utility classes
figma.com hook-only 0 22 0 No
vercel.com hook-only 0 609 1 No
linear.app hook-only 0 0 0 Yes — <html data-theme="dark">
ant.design hook-only 0 35 4 No
developer.mozilla.org both 1 1 6 No
github.com both 11,508 3,836 12 No
tailwindcss.com both 5 2 376 No
primer.style both 1,918 1,505 2 No

Read the extremes against each other. github.com carries 11,508 media-gated dark custom properties and 3,836 hook-gated ones in the same static fetch — two independent channels the audit has to name separately, exactly as a form audit must name two independent channels for a field’s name and machine purpose. linear.app sits at the other end: zero dark custom properties in either channel, because it themes with literal values, and yet it is one of only two sites dark at rest, with data-theme="dark" served on html. Channel class and darkness-at-rest are not the same measurement.

MODR: the media-only dark ratio came back zero

MODR — the media-only dark ratio — came back zero in both denominators, falsifying the hypothesis that media-only dark sites would be common; the token-level measurement from the static fetch of server-sent HTML and CSS on 2026-09-29, with no render, no JavaScript and no pointer input, found none.

Going in, the prediction was that plenty of sites would ship their dark palette only inside prefers-color-scheme, which would make them maximally invisible to default emulation. The measurement says otherwise: among dark-capable sites, MODR is 0 / 13 = 0.0%; against the full usable sample it is 0 / 20 = 0.0%. Seven of 20 sites ship no dark channel at all, and none of the remaining 13 relies on the media channel alone at token level — six are hook-only, seven expose both. Stated in the required form: MODR = sites whose only dark channel is prefers-color-scheme / sites shipping any dark channel. MODR is labeled audit-risk, never site quality: a zero means default-emulation risk is concentrated elsewhere, not that any site is doing something wrong.

TPS: the theme parity signal

TPS — the theme parity signal — is 6,939 / (6,939 + 13,515) = 6,939 / 20,454 = 0.339 across the sample, so 66.1% of the sample’s dark custom-property declarations sit inside prefers-color-scheme: dark blocks and 33.9% sit under a DOM dark hook.

The definition is TPS = dark custom properties under a DOM dark hook / (dark custom properties under a DOM dark hook + dark custom properties inside prefers-color-scheme blocks). The gate matters as much as the value: compute TPS only where the denominator is at least 1, and report undefined — never 0 — where both counts are zero, because a site with no dark custom properties has no parity ratio to report, not a parity ratio of none. color-scheme declarations are never counted in the numerator. These are lower bounds: no JavaScript ran, so hook strings living only in inline scripts are missed. The signal answers a coverage question an agent can act on, which is what a representation’s range and resolution determine.

color-scheme is not a dark-token channel

color-scheme flips UA-internal colors only — scrollbars, form controls, the canvas behind your content — and is never a dark-token channel for author styles, even though 12 of 20 sites (60%) in this sample declare the property somewhere in their served CSS.

MDN’s color-scheme reference describes it as telling the browser which color schemes it may use for its own rendering, and the W3C CSS Color Adjustment specification governs that adjustment together with forced-color-adjust; neither changes a single author-declared custom property. That is why the dataset tracks it in its own column, where those declarations sit alongside dark-token counts without ever entering them. A site can declare color-scheme: dark and ship zero dark tokens, and a site can ship thousands of dark tokens with no color-scheme declaration at all (linear.app, 0 declarations and dark at rest). Treating the property as a dark-mode signal inflates coverage; treating it as a token channel breaks both MODR and TPS.

Three objections, answered

Three objections are worth answering directly: that the audit only scoped the default theme, that a headless browser may already default to dark, and that a site which ships no dark channel at all is somehow defective by that fact.

“You only measured default-theme scoping.” The brace-walk splits by selector context, not by which theme a site calls default, so hook-only sites such as vercel.com (609 hook-gated dark custom properties) and media-gated counts such as github.com’s 11,508 are both fully counted in the same fetch. Nothing about the default palette biases the split.

“Does headless really default to light?” The Playwright documentation for the colorScheme context option states the default is light, and that default applies to any context created without an explicit value, headless or headed. The claim is not about headless engines in general; it is about one documented option.

“Sites with no dark channel are defective.” Seven of 20 usable sites ship none, and that is a product decision, not a defect. A no-dark-channel site has nothing for default emulation to miss, so it carries zero theme blind spot. MODR and TPS describe what an agent can perceive; a sibling state-blindness case, where :hover appears in none of the three at-rest representations, makes the same point without grading anyone.

Remedy: capture both states and log the probe

An agent that needs the dark palette must request it, then record that it did — emulation for media-gated sites, a root attribute for hook-only sites, and a logged matchMedia probe so that a reader can tell which state produced a number.

In Playwright, call page.emulateMedia({ colorScheme: 'dark' }), or in Python page.emulate_media(color_scheme="dark"), then re-read computed styles. Through the Chrome DevTools Protocol the correct input is Emulation.setEmulatedMedia with features: [{name: 'prefers-color-scheme', value: 'dark'}] — passing media: 'prefers-color-scheme' is invalid input and will not set the feature (CDP Emulation domain; the manual DevTools equivalent is Rendering → Emulate CSS media feature). Emulation alone does not serve hook-only sites, so for those set data-theme="dark" on the root element before calling getComputedStyle. Log the probe either way: record the boolean from matchMedia('(prefers-color-scheme: dark)').matches after each switch. The same emulation class covers prefers-reduced-motion, then sweep prefersContrast and forcedColors, checking forced-color-adjust to see whether author colors survive.

The bottom line: report theme coverage, not page quality

An audit that never sets theme state measures one theme and should say so in its own report: 13 of 20 usable sites ship a dark channel that default emulation does not resolve, and only 2 of 20 render dark at rest.

The three takeaways are straightforward. First, report the theme you actually rendered. Second, only 2 of 20 sites are dark at rest (linear.app and astro.build), so for the other eleven dark-capable sites an emulation call or an attribute injection must happen before dark tokens exist in computed styles. Third, MODR at 0/13 and 0/20 and TPS at 0.339 are coverage figures labeled audit-risk; they say what an agent can perceive, and a site shipping no dark channel is not defective. Theme coverage feeds straight into color review: the contrast floors remain 4.5:1 for normal text and 3:1 for large text under WCAG 2.2 SC 1.4.3 and 3:1 for non-text under SC 1.4.11 — but only after the dark state is in the representation you plan to measure.

Frequently asked questions

The three questions readers ask first about theme perception are whether a headless browser always defaults to light, whether a design audit should check both themes, and whether a color-scheme: dark declaration actually counts as dark mode.

Does Playwright headless always default to light?

The colorScheme option on browser.newContext() documents a default of light, and that default applies whether the browser runs headless or headed, because it is a context-level emulation setting rather than a headless-mode behavior. A context created without specifying colorScheme therefore resolves prefers-color-scheme: dark blocks as false, and dark tokens stay absent from computed styles.

Should a design audit check both themes?

Yes, and it should label each capture with the state that produced it. A theme is part of a page’s addressable states, exactly as hover is: one audit pass with default emulation and a second pass with dark emulation, each logging the matchMedia result. Report coverage per state rather than a single blended score.

Does color-scheme: dark count as dark mode?

No. color-scheme tells the user agent which schemes it may use for its own internal rendering — scrollbars, form controls, canvas — as the W3C CSS Color Adjustment specification defines it. It never selects author dark tokens, so it is not a dark-token channel and is excluded from both the MODR and TPS numerators, even though 12 of 20 sites declare it.

How this guide was built

This post is desk research plus a static fetch of server-sent HTML and CSS — no rendering, no JavaScript, no pointer input and no assistive-technology testing — and every number below is reproducible from the published measurement script.

The dataset is a 20-site sample measured on 2026-09-29 with two exclusions (adobe.com read timeout; w3.org/WAI 403), and the brace-walk methodology is described in the method section above. Feature provenance for prefers-color-scheme traces to web.dev’s explainer. The measurement script and this slot’s reruns live in the arena. Related state-blindness work: the three representations and their resolution and the :hover blind spot.