Dark mode
Every token ships a light and a dark value. Dark mode is a single attribute on the html element, with no .dark class per component. The only media query in the library is in the block editor, which follows the OS setting when no attribute is set.
Try it on this page
One attribute on the html element flips every token.
<html data-theme="dark">The mechanism
Set data-theme="dark" (or "light") on <html>. The token stylesheet emits both a base block and a :where(:root[data-theme="dark"]) block, so flipping the attribute re-values every --oks-* property at once.
document.documentElement.setAttribute("data-theme", "dark");Respecting the system preference
With no attribute set, fall back to the OS setting in your own CSS:
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
/* your dark overrides, if any */
}
}No flash of the wrong theme
Apply the stored / preferred theme before the page paints with a tiny blocking script in <head>. Without this, a user who prefers dark sees a white flash on every load.
<script>
(function () {
try {
var stored = localStorage.getItem("theme");
var dark = stored === "dark" ||
(!stored && matchMedia("(prefers-color-scheme: dark)").matches);
document.documentElement.setAttribute("data-theme", dark ? "dark" : "light");
} catch (e) {}
})();
</script>layout.tsx via next/script with strategy="beforeInteractive", and add suppressHydrationWarning to the <html> element (the script mutates it before React hydrates).Surfaces and form fields follow the theme too
--oks-color-surface and --oks-form-field-bg have dark values built in, and the other --oks-form-field-* tokens are built from the neutral palette, which flips in dark mode. You only need to set them yourself if your brand uses different surfaces, and if you do, set both the light and the dark value.