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.