Dark mode
One class on <html> drives the whole system. Components never use dark: prefixes — they reference semantic tokens that flip underneath them.
How it works
globals.css registers a custom variant:
Then theme.css redefines the semantic layer inside a .dark rule. A component that says bg-background-full gets the right surface in either theme without knowing a theme exists.
The toggle component
theme-toggle (https://boardcn.dev/r/theme-toggle.json) is a switch that reveals the next theme from the exact point you clicked, using a view transition. It persists to localStorage under boardcn:theme and does not depend on the system theme.
To drive the theme from your own control instead:
Avoiding the flash
If the theme is applied after hydration, the page paints light and then flips. Run a blocking script in <head> so the class is set before first paint — this site does exactly this:
Read the same boardcn:theme key the toggle writes, so the two never disagree. Add suppressHydrationWarning to <html>, since the script mutates it before React sees it.
Details worth knowing
- The toggle dispatches a
boardcn:theme-changeevent, so other components can react without prop drilling. - Native view-transition crossfades are disabled deliberately; only the circular clip-path reveal runs. Browsers without support just switch instantly.
- Chart colors are theme-aware through the same token layer — see Colors.