Theme Switching
Set up light/dark mode with UI Lab using an app-owned theme.css, cookie persistence, and a server-stamped Next.js layout.
Theme Switching
The primary UI Lab integration path is now script-free:
- Keep your token definitions in your app's own
theme.css. - Import
ui-lab-components/styles.cssafter that local theme file. - Persist explicit
"light"or"dark"overrides in a cookie. - In Next.js, read that cookie in the server
app/layout.tsxand stamp<html>with the server-safe helpers fromui-lab-components/theme-server. - Leave system mode unstamped so
prefers-color-schemehandles first paint without an inline bootstrap script.
ui-lab-components/theme-script still exists, but it is now the fallback path for setups that cannot use a server layout plus cookie persistence.
The important contract stays the same: UI Lab does not calculate colors at runtime. Your CSS owns the tokens, and the active mode only changes which CSS block applies.
Recommended contract
For normal app theming:
- Own the token layer in
app/theme.cssor equivalent - Import that file before
ui-lab-components/styles.css - Let system mode come from CSS via
prefers-color-scheme - Stamp explicit light/dark overrides onto
<html>from the server - Use
useColorMode()for user-facing toggles
@ui does not need to ship a theme.css for consumers. Most apps already own that syntax layer, and UI Lab is designed to consume the active --color-* tokens you expose there.
Use ThemeProvider only when you need runtime token batches, a theme editor, or custom theme persistence beyond a normal light/dark mode choice.
1. Import your theme before UI Lab styles
In your app stylesheet:
The order matters:
tailwindcssestablishes the layerstheme.cssdefines your app's tokens and mode rulesui-lab-components/styles.cssconsumes the active tokens
2. Keep token definitions in app-owned theme.css
A concise pattern looks like this:
Important details:
- Your app owns this file. UI Lab only consumes the active tokens.
:root:not([data-theme])is the system path. No cookie override means CSS decides first paint.:root[data-theme="light"]and:root[data-theme="dark"]are explicit overrides.@theme inlinekeeps Tailwind utilities stable by mapping--color-*tokens to your active variables.
This is why markup like bg-background-950 can stay unchanged while the token values switch underneath it.
3. Stamp <html> from the server layout
In Next.js, read the cookie in app/layout.tsx and apply the resolved root state:
parseThemeCookie() accepts the cookie value and returns "light", "dark", "system", or null. resolveThemeRootState() only stamps explicit light/dark overrides:
"light"setsclassName,data-theme, andstyle.colorSchemeto"light""dark"setsclassName,data-theme, andstyle.colorSchemeto"dark""system"or no cookie returns{}, which leaves<html>unstamped
That unstamped system state is the reason this path avoids FOUC without an inline script.
4. Toggle the theme from a client component
useColorMode() updates the DOM, persists the explicit mode in storage, and writes the ui-lab-theme cookie used by the server layout on the next request.
themeMode is always the resolved mode, "light" or "dark". If there is no explicit override, the initial mode follows prefers-color-scheme.
Fallback: theme-script
If you cannot use a cookie-backed server layout, ui-lab-components/theme-script is still available as a fallback for non-server or non-cookie setups.
That path is no longer the primary recommendation for Next.js apps. Prefer:
- app-owned
theme.css @import "./theme.css";@import "ui-lab-components/styles.css";- cookie persistence for explicit overrides
- server-stamped
<html>viaparseThemeCookie()andresolveThemeRootState()
What changed from older setup examples
If you have older notes or examples, update these assumptions:
- Do not treat
generateColorModeScript()inapp/layout.tsxas the default Next.js setup. The cookie plus server-layout path is now preferred. - Do not import
ui-lab-components/theme.css. Consumers usually already own the token layer in their app'stheme.css. - Do not stamp system mode onto
<html>. Leave it unset soprefers-color-schemehandles first paint. - Do not assume UI Lab ships your token definitions. UI Lab only consumes the active token contract.
- Do not add
ThemeProviderjust to get a basic light/dark toggle.
Further reading
- Theming for the token model and color roles
- Theme configurator for generating a custom palette