Skip to content

August 15, 2026 · 3 min read

"Beyond light and dark: theming with independent families and modes"

"Theme families let users switch colors independently from light/dark mode — giving you plum-light, plum-dark, mint-light, mint-dark, and every combination from a single API."

familiesmodesconcepts

A single light/dark toggle is enough for many apps, but it breaks down when you need more than one palette. A corporate dashboard might offer "Plum" and "Mint" families — each with its own light and dark mode. With families, those modes are independent dimensions.

The problem with one-dimensional toggles

A typical dark mode implementation stores a single boolean: isDark. Your app is either light or dark. But what if you want:

  • A Plum palette (purple primary) with both light and dark modes
  • A Mint palette (green primary) with its own light and dark
  • A High Contrast family for accessibility

With a boolean toggle, every palette is a separate theme. You'd need plum-light, plum-dark, mint-light, mint-dark — and the user can't switch palette without switching mode.

Families: two independent dials

Theme Kit separates the concept of family (the color palette) from mode (light/dark/system):

ts
1import { useTheme } from "@theme-kit/react";
2
3function ThemePicker() {
4  const { theme, mode, family, setMode, setFamily, toggleTheme } = useTheme();
5
6  return (
7    <div>
8      <select value={family} onChange={(e) => setFamily(e.target.value)}>
9        <option value="plum">Plum</option>
10        <option value="mint">Mint</option>
11        <option value="cocoa">Cocoa</option>
12      </select>
13
14      <button onClick={toggleTheme}>
15        Switch to {mode === "light" ? "dark" : "light"}
16      </button>
17
18      <p>Active: {theme.name}</p>
19    </div>
20  );
21}

The user picks a family (Plum → Mint) and a mode (light → dark) independently. The runtime resolves family: "plum", mode: "dark" to plum-dark automatically.

How themes are organized

Themes in Theme Kit carry a meta field that declares their family and mode:

ts
1import { defineTheme } from "@theme-kit/core";
2
3export const plumLight = defineTheme({
4  name: "plum-light",
5  meta: { family: "plum", mode: "light", label: "Plum Light" },
6  tokens: { /* ... */ },
7});
8
9export const plumDark = defineTheme({
10  name: "plum-dark",
11  meta: { family: "plum", mode: "dark", label: "Plum Dark" },
12  tokens: { /* ... */ },
13});

The registry indexes themes by family and mode, so resolveInitialTheme({ themes, family: "plum", mode: "system" }) resolves to the correct theme — and re-resolves when the user switches family or the OS preference changes.

The nine built-in families

Theme Kit ships nine families out of the box — oat, berry, mint, citrus, cocoa, plum, iris, sky, and graphite — each with a light and dark mode. You can add your own by defining themes with the same meta.family and meta.mode structure.

Families in the runtime

The runtime's selection controller manages both dimensions:

ts
1runtime.selection.setFamily("mint");  // switches the palette
2runtime.selection.setMode("dark");    // switches the mode
3runtime.selection.toggleTheme();      // light ↔ dark, same family
4
5// Read the current state
6const { family, mode } = runtime.selection.getSelection();

The theme resolves forward: family + mode → theme name → theme definition. This means every family/mode combination is valid, even if you haven't defined a theme for it — the runtime falls back to the family's default.

Why families matter

  • User choice — let users pick both personality (family) and comfort (mode)
  • Brand consistency — every family carries its own on-brand colors, but the mode switch is independent
  • Accessibility — a high-contrast family coexists with your main palette, and users can combine it with their preferred mode
  • SSR-safe — both dimensions are persisted, bootstrapped, and restored across reloads with no extra effort
"Beyond light and dark: theming with independent families and modes" — Theme Kit