@k8ordo/color-scheme

Troubleshooting

Common symptoms, what causes them, and how to fix them.

On this page

With dark chosen, a reload flashes light first

Cause

The inline script did not run before the page was painted. Most often a CSP blocked it, or the provider sits in a nested layout, so whatever comes before it is painted first.

Fix

If the console reports a CSP violation, see “A CSP blocks the script” below. Otherwise, move the provider into the root layout, inside <body>, around the whole page.

A CSP blocks the script

Cause

A policy that restricts scripts does not allow the inline one. When it is allowed by hash, the hash may have been computed for a different defaultPreference than the provider’s, or copied from an older version.

Fix

Under @k8ordo/server, give the provider nonce={nonce()}. By hash, pass colorSchemeScriptHash() the provider’s defaultPreference and compute it wherever the policy is written, every time. Do not reach for 'unsafe-inline', which also allows any other inline script that reaches the page.

React reports a hydration mismatch on <html>

Cause

The class="dark" the inline script added is not in the server’s HTML. In development React compares <html>’s attributes with the props it renders and reports the class as A tree hydrated but some attributes of the server rendered HTML didn't match.

Fix

Give <html> suppressHydrationWarning. It silences <html>’s own attributes only; mismatches inside the page are still reported. Hydration does not write attributes back, so the class stays.

Scrollbars and form controls do not match the chosen scheme

Cause

What the browser draws itself follows the CSS color-scheme property. This package does not set it, so without @k8ordo/ui nothing declares it. Declared as color-scheme: light dark, it lets the browser pick by the OS setting, which disagrees with the visitor’s choice.

Fix

Declare color-scheme: light on :root and color-scheme: dark on .dark, so the property follows the class too.

Tailwind CSS’s dark: ignores the choice

Cause

Tailwind CSS 4’s dark: reads prefers-color-scheme by default, so it follows the OS setting but not what the visitor chose.

Fix

Declare @custom-variant dark (&:where(.dark, .dark *)); in the stylesheet so dark: reads the class. @k8ordo/ui’s tailwind.css already declares it.

An icon or label shows the other scheme just after loading

Cause

A server cannot read the stored choice, so it renders the default. Markup chosen from scheme shows that guess until hydration.

Fix

Render both and hide one with dark:, or call use(browser()) inside a <Suspense> so it renders in the browser only. See “Build a switch”.

useColorScheme needs <ColorSchemeProvider> above it

Cause

There is no provider above the component that called useColorScheme(). Usually the provider sits in a nested layout, or a test renders without a wrapper.

Fix

Put the provider in the root layout, inside <body>, around the whole page. In a test, pass it to renderHook as the wrapper.

Changing the OS setting does nothing

Cause

The OS setting is followed only while the visitor has chosen nothing; once they pick light or dark, that choice wins. With a defaultPreference other than 'system', the OS setting is not used either.

Fix

setPreference('system') clears the stored choice, and the default applies again. Offer a three-way choice that includes the system so visitors can go back themselves.

Writing to localStorage changes nothing

Cause

Tabs pass the preference along with the storage event, which never reaches the tab that wrote. A localStorage.setItem in the same tab therefore goes unnoticed by the provider until a reload.

Fix

Change the choice through setPreference. Other tabs with the site open follow along.

Tests log “Encountered a script tag”

Cause

Rendered only in the browser, as in a test, React does not execute the provider’s inline <script>, and its development build logs that as an error.

Fix

Nothing failed, and there is nothing to fix. The class a test sees on <html> is the one the provider’s effect wrote.

k8ordo

React libraries that use Baseline features without holding back.

© 2026 k8o — MIT License

Typeset in Noto Sans JP & M PLUS 2