Dark mode
Apply the dark theme with StyleX createTheme — no flash, portals included.
Dark mode is a StyleX theme: lib/themes.ts exports darkTheme, a
stylex.createTheme(colors, { … }) that overrides every color token. Apply
it and all components follow — there is no per-component dark styling.
Where to apply it
Always on <html> (or <body>), never a wrapper div: dialogs, popovers,
menus, and toasts portal to <body>, so a subtree theme would not reach them.
import * as stylex from '@stylexjs/stylex';
import { darkTheme } from '@/lib/themes';
const { className } = stylex.props(dark && darkTheme);
document.documentElement.className = className ?? '';
Toggle with React state
'use client';
import * as React from 'react';
import * as stylex from '@stylexjs/stylex';
import { Button } from '@/components/ui/button';
import { darkTheme } from '@/lib/themes';
export function ThemeToggle() {
const [dark, setDark] = React.useState(false);
React.useEffect(() => {
const { className } = stylex.props(dark && darkTheme);
document.documentElement.className = className ?? '';
}, [dark]);
return (
<Button variant="secondary" onClick={() => setDark(!dark)}>
{dark ? 'Light mode' : 'Dark mode'}
</Button>
);
}
Avoiding the flash of light mode
An effect runs after first paint — a stored dark preference would flash light
first. Stamp the theme class before paint with an inline script in
<head>. The theme’s class name comes from StyleX, so render it into the
script from a shared constant:
// app/layout.tsx (Next.js)
import * as stylex from '@stylexjs/stylex';
import { darkTheme } from '@/lib/themes';
const darkClassName = stylex.props(darkTheme).className ?? '';
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<script
dangerouslySetInnerHTML={{
__html: `if (localStorage.theme === 'dark' || (!('theme' in localStorage) && matchMedia('(prefers-color-scheme: dark)').matches)) document.documentElement.className = ${JSON.stringify(darkClassName)};`,
}}
/>
</head>
<body>{children}</body>
</html>
);
}
Store the preference when toggling (localStorage.theme = 'dark' | 'light')
and the script restores it on every load, including hard refreshes.
System preference only
If you don’t need a manual toggle, skip the storage and follow the OS:
const prefersDark = matchMedia('(prefers-color-scheme: dark)');
const apply = () => {
const { className } = stylex.props(prefersDark.matches && darkTheme);
document.documentElement.className = className ?? '';
};
apply();
prefersDark.addEventListener('change', apply);
More themes
darkTheme is just a theme — create as many as you like
(stylex.createTheme(colors, { … }) in lib/themes.ts) and apply the same
way. See Customization for the token contract.