Skip to content

fix(web): set the color scheme on the root element, plus coverage - #428

Open
YevheniiKotyrlo wants to merge 3 commits into
nativewind:mainfrom
YevheniiKotyrlo:fix/small-findings
Open

YevheniiKotyrlo wants to merge 3 commits into
nativewind:mainfrom
YevheniiKotyrlo:fix/small-findings

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Aug 15, 2026 •

Copy link
Copy Markdown
Contributor

Problem

On main:

  1. colorScheme.set throws a TypeError on web. src/web/api.tsx calls Appearance.setColorScheme, and react-native-web 0.21's Appearance has no such method. TypeScript can't see this, because the file imports Appearance from react-native and the bundler substitutes react-native-web.
  2. The metro globalClassNamePolyfill gate is untested. The resolver rewrites react-native imports only when the option is on, and neither side of that branch has a test.
  3. The prefers-color-scheme compile path is untested. Only light-dark() reaches the condition in a test, and there the compiler synthesises it.

Solution

  • On web, colorScheme.set("dark") and set("light") put that class on <html> and set its color-scheme, and set(null) returns the root to the system. No page can override prefers-color-scheme, so the root is where a forced scheme can reach web styles. Measured in Chromium 153, Firefox 155 and WebKit 26.6, a dark variant on a .dark class, :root.dark variables and light-dark() follow it, and a rule inside @media (prefers-color-scheme: dark) keeps the system's answer.
  • get() reads the class back, so a scheme an app put on the root itself reads the same, and it falls back to "light" as on native. Without a document, as while a route prerenders, set() does nothing. The README documents both platforms.
  • src/__tests__/metro/global-class-name-polyfill.test.ts covers both sides of the gate:
    • the native and web dispatch;
    • the override module;
    • a config's own resolveRequest;
    • the css source extension.
  • src/__tests__/compiler/media-query.test.ts compiles (prefers-color-scheme: dark) and (prefers-color-scheme: light).

Each finding is its own commit.

Tests

  • src/__tests__/web/color-scheme.test.ts, in jsdom:
    • pins that react-native-web's Appearance has no setter, so the case fails if one ever appears and set can forward to it;
    • each scheme marks the root and reads back, and a later one replaces it;
    • null, undefined and "unspecified" return the root to the system;
    • get() reads a class the app set itself, and the root keeps its other classes and inline styles.
  • src/__tests__/web/color-scheme-prerender.test.ts sets a scheme with no document.
  • On main, 8 of the 10 web cases fail. The metro and compile cases pass there, since they cover existing behaviour.
  • Each of 12 mutations fails its own cases. Seven are on the web setter: the class, color-scheme, the clearing of either, the read-back, the prerender guard and the light fallback. The other five: the inverted gate, web dispatching to the native resolver, a config's resolver ignored, css dropped from the source extensions, and an ident media value lost.

Verification

Every ci.yml job but the iOS build, on Ubuntu with Node 22.18.0: green, with yarn test at 1355 passed.

Known limits

  • On web a rule inside @media (prefers-color-scheme) keeps the system's answer, since no page can override that media feature. A theme follows a forced scheme through a class on the root or through light-dark().

Merge order

It shares lines with #411 (README.md) and #430 (README.md); whichever lands second rebases.

Base

Re-written on main (a5002c5). Two findings are gone from this PR:

@YevheniiKotyrlo

YevheniiKotyrlo commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor Author

No device frame

The change is to colorScheme.set on web, which now puts the scheme on <html>; the description lists what Chromium, Firefox and WebKit do with it. The other findings are test and CI coverage.

On web, colorScheme.set() called Appearance.setColorScheme, which
react-native-web does not have, so it threw a TypeError. No page can
override prefers-color-scheme either, so a scheme the app forces cannot
reach a rule inside @media (prefers-color-scheme) on web.

set("dark") and set("light") now put that class on <html> and set its
color-scheme, which a dark variant declared on a .dark class and every
light-dark() value follow; set(null) returns the root to the system.
get() reads the class back, so a scheme the app put on the root itself
reads the same, and falls back to "light" as on native. Without a
document, as while a route prerenders, set() does nothing.
The metro resolver rewrites react-native imports only when
globalClassNamePolyfill is on, and nothing tested either side of that gate.
Only light-dark() reached prefers-color-scheme in a test, and the compiler
synthesises that condition itself, so the parse of an authored
(prefers-color-scheme: dark) was untested.
@YevheniiKotyrlo YevheniiKotyrlo changed the title fix: six small findings, plus a windows CI lane fix(web): set the color scheme on the root element, plus coverage and a windows CI lane Oct 11, 2026
@YevheniiKotyrlo YevheniiKotyrlo changed the title fix(web): set the color scheme on the root element, plus coverage and a windows CI lane fix(web): set the color scheme on the root element, plus coverage Oct 11, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant