Repository navigation
fix(web): set the color scheme on the root element, plus coverage - #428
Open
YevheniiKotyrlo wants to merge 3 commits into
Open
YevheniiKotyrlo wants to merge 3 commits into
YevheniiKotyrlo wants to merge 3 commits into
Conversation
Contributor
Author
No device frameThe change is to |
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
force-pushed
the
fix/small-findings
branch
from
October 11, 2026 12:36
7c64764 to
0bdfc07
Compare
YevheniiKotyrlo
force-pushed
the
fix/small-findings
branch
from
October 11, 2026 18:15
0bdfc07 to
5d46f79
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On
main:colorScheme.setthrows aTypeErroron web.src/web/api.tsxcallsAppearance.setColorScheme, and react-native-web 0.21'sAppearancehas no such method. TypeScript can't see this, because the file importsAppearancefromreact-nativeand the bundler substitutes react-native-web.globalClassNamePolyfillgate is untested. The resolver rewritesreact-nativeimports only when the option is on, and neither side of that branch has a test.prefers-color-schemecompile path is untested. Onlylight-dark()reaches the condition in a test, and there the compiler synthesises it.Solution
colorScheme.set("dark")andset("light")put that class on<html>and set itscolor-scheme, andset(null)returns the root to the system. No page can overrideprefers-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.darkclass,:root.darkvariables andlight-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.tscovers both sides of the gate:resolveRequest;csssource extension.src/__tests__/compiler/media-query.test.tscompiles(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:Appearancehas no setter, so the case fails if one ever appears andsetcan forward to it;null,undefinedand"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.tssets a scheme with no document.main, 8 of the 10 web cases fail. The metro and compile cases pass there, since they cover existing behaviour.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,cssdropped from the source extensions, and an ident media value lost.Verification
Every
ci.ymljob but the iOS build, on Ubuntu with Node 22.18.0: green, withyarn testat 1355 passed.Known limits
@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 throughlight-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:prefers-color-schemeresolving tolightwith no OS preference is now in fix: evaluate range pairs, and answer a condition the runtime cannot decide with unknown #426, which evaluates the feature, and fix(native): make colorScheme.set move every reader of the scheme #415, which resolves the scheme.main's tests now keep:rootvariables dynamic with{ inlineVariables: false }, so thedynamicRootVariableshelper is dropped.