Dark mode
One attribute. There is no dark variant of any component.
Every token is defined twice — under :root and under [data-theme="dark"]. Components read
tokens, so switching the attribute reprints the whole tree.
<html data-theme="dark">That is the mechanism in full. No dark: utilities in component files, no second set of styles
to keep in sync.
With Fumadocs or next-themes
Write both attributes, so Tailwind's dark: variant and the token layer agree:
<RootProvider theme={{ attribute: ['class', 'data-theme'], defaultTheme: 'light' }}>Tell the browser too
Scrollbars, native controls and the default focus ring are painted by the browser, not by tokens:
html { color-scheme: light; }
html[data-theme='dark'] { color-scheme: dark; }Without it you get dark panels with a bright white scrollbar down the side.
Theming a subtree
data-theme works anywhere, not only on the root — useful for a viewer that should stay dark
whatever the surrounding application is doing:
<div data-theme="dark">
{/* resolves dark tokens regardless of the page */}
</div>Anything hardcoding a colour breaks this. A stray #fff is invisible in light mode and
glaring in dark — by a wide margin the most common source of dark-mode bugs.