A complete mirror of the light-mode token set, tuned for dark surfaces. The brand stays iris, but the anchor shifts from iris-500 to iris-300 so accents stay readable. Neutrals drop into a violet-tinted near-black instead of pure black — warmer, and consistent with the brand.
Theme · darkBackground, surface, ink, and rule — mirrored. Dark mode uses a violet-black (#0B0A14) rather than pure black; it reads as deliberate and keeps brand warmth.
On dark, step 500 gets muddy. The anchor moves to step 300 so accents keep their punch, and paired muted backgrounds drop to step 800.
Identical content, both themes. Every color comes from a role alias; the component code doesn't change.
Give it a name and invite teammates. You can change both later.
Give it a name and invite teammates. You can change both later.
On dark, shadows disappear into the background. Elevation is communicated by lifting the surface lightness one step at a time — and borders get a touch stronger.
Dark mode is a translation, not a reskin. Same layout, same hierarchy, same semantics.
Components reference --color-primary, not --iris-500. The alias resolves differently per theme — the component doesn't need to know.
A flipped palette always looks off. The dark set is tuned per-token — lightness, chroma, and contrast are all re-balanced, not reflected.
Shadows vanish against black. Use a one-step lighter surface for elevation; reserve real shadows for popovers and modals floating over content.
#000 and #FFF glare. Ink is #F5F3FF (subtle iris warmth); background is #0B0A14 — violet-black, never flat black.
Every alias has to clear WCAG AA in both themes. Coral-500 on white passes; coral-500 on dark fails — so dark uses coral-300.
Viz palettes need their own dark tuning — line strokes thicken by .5px, gridlines use --d-rule, fills drop opacity to 0.6 so underlying grid stays visible.
Wrap in [data-theme="dark"] on <html> or <body>. Tokens cascade everywhere.
[data-theme="dark"] {
/* Neutrals */
--bg: #0B0A14;
--bg-elev: #151427;
--bg-elev-2: #1E1C36;
--ink: #F5F3FF;
--ink-muted: #A8A3C7;
--ink-subtle: #6E6A8E;
--rule: #2A2744;
--rule-strong: #3A3658;
/* Role aliases — anchor shifts to step 300 */
--color-primary: var(--iris-300);
--color-primary-fg: var(--iris-950);
--color-primary-hover: var(--iris-200);
--color-primary-muted: var(--iris-800);
--color-primary-subtle:#1A0D52;
--color-secondary: var(--coral-300);
--color-secondary-fg: var(--coral-950);
--color-secondary-hover: var(--coral-200);
--color-secondary-muted: var(--coral-800);
--color-accent: var(--teal-300);
--color-accent-fg: var(--teal-950);
--color-accent-hover: var(--teal-200);
--color-accent-muted: var(--teal-800);
/* State — lifted 200 steps for dark contrast */
--color-success: #34D399;
--color-warning: #FBBF24;
--color-danger: #F87171;
--color-info: #60A5FA;
/* Elevation uses surface, not shadow */
--shadow-sm: none;
--shadow-md: 0 4px 12px -4px rgba(0,0,0,.5);
--shadow-lg: 0 20px 40px -20px rgba(0,0,0,.7);
}
/* Optional — follow OS preference */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
/* repeat block above */
}
}