Light and dark

Every Tostada library renders in both light and dark mode. The same source-of-truth tokens drive both — you only override what actually differs.

The resolution chain

A semantic or component token has up to four relevant fields:

  • value — its own raw value (used by primitives)
  • references — points at another token in light mode
  • darkValue — its own raw value in dark mode
  • darkReferences — points at another token in dark mode

When Tostada needs the light value of a token, it walks: references → that token's value → continue if that token also has references.

When Tostada needs the dark value, it walks: darkReferences (if set) → fall through to references → that token's value, then darkValue (if set) → fall through to value.

In plain English: dark mode tries the dark chain first; if no dark override exists at any level, it uses the light chain.

What the export looks like

The generated tokens.css has two blocks:

:root {
  /* light values for every token */
  --color-blue-500: #1B19FF;
  --bg-accent: var(--color-blue-500);
  --primary: var(--bg-accent);
}

.dark {
  /* ONLY the tokens whose dark value differs from light */
  --bg-base: var(--color-gray-950);
  --fg-default: var(--color-gray-50);
  --primary: var(--bg-accent-dark);
}

If a token's dark resolution equals its light resolution, it's omitted from the .dark block. This keeps the file lean and your intent clear: anything in .dark is an intentional override.

Authoring patterns

  • Override surfaces, not the accent. A typical brand color works in both modes — only bg-base, fg-default, and border-default usually need dark overrides.
  • Override at the semantic tier when possible. Setting bg-accent.darkReferences propagates to every component variable bound to it. Overriding --primary directly only fixes one binding.
  • Empty is fine. Leaving darkReferences empty isn't an error — it means "use the light chain in dark mode."

Anti-patterns

  • Forking a full dark palette. If 90% of your dark tokens are the same as light, don't set 90% of darkReferences. Only override what differs.
  • Overriding at the component tier when the semantic would do. It works, but you'll edit five component vars instead of one semantic.
  • Setting both darkValue and darkReferences. darkReferences wins. Pick one per token.

Where this lives in code

  • Resolution: src/lib/docs.tsgenerateTokensCss
  • Token shape: src/tokens/types.tsdarkValue and darkReferences are both optional fields on DesignToken
  • UI: dark-mode toggle in the top nav re-broadcasts the resolved CSS to every preview iframe