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 modedarkValue— its own raw value in dark modedarkReferences— 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, andborder-defaultusually need dark overrides. - Override at the semantic tier when possible. Setting
bg-accent.darkReferencespropagates to every component variable bound to it. Overriding--primarydirectly only fixes one binding. - Empty is fine. Leaving
darkReferencesempty 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
darkValueanddarkReferences.darkReferenceswins. Pick one per token.
Where this lives in code
- Resolution:
src/lib/docs.ts—generateTokensCss - Token shape:
src/tokens/types.ts—darkValueanddarkReferencesare both optional fields onDesignToken - UI: dark-mode toggle in the top nav re-broadcasts the resolved CSS to every preview iframe