Skip to content

Dark Mode

Theme switching with the data-theme attribute, persistence, and flash prevention.

Dark mode is the default. @diffgazer/ui uses the [data-theme] attribute for theme switching.

How it works

css
/* Dark mode (default) */
:root,
[data-theme="dark"] {
  --base-bg: #0a0a0a;
  --base-fg: #e5e5e5;
  /* ... */
}

/* Light mode */
[data-theme="light"] {
  --base-bg: #f7f8f5;
  --base-fg: #1f2328;
  /* ... */
}

All @diffgazer/ui components use semantic CSS variables (var(--background), var(--foreground), etc.) which reference the primitives. Changing the data-theme attribute switches every component instantly.

Subtree theming

data-theme is not limited to <html>. Because the Tailwind bridge maps tokens with @theme inline, every token utility resolves its semantic variable at the styled element — so setting data-theme on any element re-themes that subtree only:

tsx
<div data-theme="light">
  {/* This panel renders in light mode inside an otherwise-dark page. */}
  <Panel></Panel>
</div>

This is how a theme preview can show light and dark side by side without re-declaring the token table.

Switching themes

Set the data-theme attribute on the root element:

tsx
function ThemeToggle() {
  const [theme, setTheme] = useState<"dark" | "light">("dark")

  const toggle = () => {
    const next = theme === "dark" ? "light" : "dark"
    setTheme(next)
    document.documentElement.setAttribute("data-theme", next)
  }

  return (
    <Button variant="ghost" onClick={toggle}>
      {theme === "dark" ? "light" : "dark"}
    </Button>
  )
}

Persisting theme preference

Store the user's choice in localStorage:

tsx
// On mount: restore preference
useEffect(() => {
  const saved = localStorage.getItem("@diffgazer/ui-theme")
  if (saved === "light" || saved === "dark") {
    document.documentElement.setAttribute("data-theme", saved)
  }
}, [])

// On change: persist
const setTheme = (theme: "dark" | "light") => {
  document.documentElement.setAttribute("data-theme", theme)
  localStorage.setItem("@diffgazer/ui-theme", theme)
}

Respecting system preference

tsx
useEffect(() => {
  const saved = localStorage.getItem("@diffgazer/ui-theme")
  if (saved) {
    document.documentElement.setAttribute("data-theme", saved)
    return
  }

  const prefersDark = window.matchMedia("(prefers-color-scheme: dark)")
  document.documentElement.setAttribute(
    "data-theme",
    prefersDark.matches ? "dark" : "light"
  )
}, [])
Success:

Tip: @diffgazer/ui defaults to dark mode. If no data-theme attribute is set, the :root selector applies dark mode variables.

Flash prevention

To prevent a flash of wrong theme on page load, set the attribute before React hydrates:

html
<head>
  <script>
    (function() {
      var t = localStorage.getItem("@diffgazer/ui-theme");
      if (t) document.documentElement.setAttribute("data-theme", t);
    })();
  </script>
</head>