Recipes

Recipe: Theme switcher with persistence

A persistent dark-mode toggle using the built-in dark engine.

Drop-in (one line)

import { ThemeToggle } from "traceless-style/dark";

<header>
  <ThemeToggle />
</header>

That's it — <ThemeToggle /> toggles .dark on <html>, persists in localStorage, and re-renders other useTracelessDark() consumers. Combined with auto-dark-mode on every color value in your styles, this is the entire integration.

Add the anti-flash script to your root layout:

// app/layout.tsx
import { TracelessRoot } from "traceless-style/dark";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head><TracelessRoot /></head>
      <body>{children}</body>
    </html>
  );
}

Custom toggle button

import { useTracelessDark } from "traceless-style/dark";
import { tl } from "traceless-style";

const $ = tl.create({
  btn: {
    padding:      "0.5rem 1rem",
    borderRadius: "999px",
    border:       "1px solid currentColor",
    background:   "transparent",
    color:        "inherit",
    cursor:       "pointer",
    fontSize:     "0.875rem",

    _hover:        { background: "rgba(0,0,0,0.05)" },
    _focusVisible: { outline: "2px solid currentColor", outlineOffset: "2px" },
  },
});

export function MyToggle() {
  const { isDark, toggle, mode } = useTracelessDark();

  return (
    <button className={$.btn} onClick={toggle} aria-pressed={isDark}>
      {isDark ? "🌙 Dark" : "☀️ Light"}
      <span aria-hidden style={{ opacity: 0.6 }}>({mode})</span>
    </button>
  );
}

Three-way switch (dark / light / system)

import { useTracelessDark } from "traceless-style/dark";

export function ThemeRadios() {
  const { mode, set, system } = useTracelessDark();

  return (
    <fieldset>
      <legend>Theme</legend>
      <label><input type="radio" checked={mode === "light"}  onChange={() => set("light")} /> Light</label>
      <label><input type="radio" checked={mode === "dark"}   onChange={() => set("dark")}  /> Dark</label>
      <label><input type="radio" checked={mode === "system"} onChange={system}             /> System</label>
    </fieldset>
  );
}

Listening from outside React

import { dark } from "traceless-style/dark";

dark.subscribe(mode => {
  // notify a third-party chart, tracking pixel, etc.
  window.dataLayer?.push({ event: "theme_change", theme: mode });
});

Multiple themes (dark + brand A/B + density)

const dark    = tl.createTheme("dark",    { /* color overrides */ });
const brandB  = tl.createTheme("brand-b", { brand: { primary: "#ec4899" } });
const compact = tl.createTheme("compact", { spacing: { md: "0.5rem" } });

<body className={tl.cx(dark, brandB, compact)}>...</body>

See also

See also