Theming System
Switch between light, dark, and brand themes by toggling a data-theme attribute — one component, three looks, zero JavaScript needed for the CSS.
Quick implementation
/* Define tokens per theme */
[data-theme="light"] {
--t-bg: oklch(0.97 0.005 260);
--t-card: oklch(1 0 0);
--t-text: oklch(0.2 0.01 260);
--t-muted: oklch(0.5 0.01 260);
--t-accent: oklch(0.52 0.22 265);
--t-border: oklch(0.88 0.01 260);
}
[data-theme="dark"] {
--t-bg: oklch(0.13 0.02 260);
--t-card: oklch(0.19 0.02 260);
--t-text: oklch(0.93 0.01 260);
--t-muted: oklch(0.55 0.02 260);
--t-accent: oklch(0.72 0.19 265);
--t-border: oklch(1 0 0 / 0.1);
}
[data-theme="brand"] {
--t-bg: oklch(0.25 0.12 290);
--t-card: oklch(0.32 0.14 285);
--t-text: oklch(0.95 0.02 290);
--t-muted: oklch(0.72 0.08 290);
--t-accent: oklch(0.82 0.18 55);
--t-border: oklch(1 0 0 / 0.15);
}
/* Components consume tokens — same CSS, any theme */
.card {
background: var(--t-card);
color: var(--t-text);
border: 1px solid var(--t-border);
}Prompt this to your LLM
Includes role, constraints, two framework variants, and edge cases to handle.
You are a CSS design systems engineer. Build a multi-theme CSS system using data-theme attributes and CSS custom properties.
Requirements:
1. Define semantic color tokens (--t-bg, --t-card, --t-text, --t-muted, --t-accent, --t-border) for three themes: light, dark, and brand.
2. Use [data-theme="light"], [data-theme="dark"], [data-theme="brand"] attribute selectors — NOT :root overrides.
3. Use oklch() for all color values.
4. Write a card component (.card) that consumes only the semantic tokens — zero hardcoded colors.
5. Show how switching data-theme on a parent element instantly re-skins all children.
Framework variants:
A) Vanilla — toggle data-theme with a <select> and one line of JavaScript: document.body.dataset.theme = value.
B) React — a ThemeProvider context that writes data-theme to a wrapper div.
Edge cases:
- Prefix tokens with a namespace (--t-) to avoid collisions with global site tokens.
- Ensure sufficient contrast in all three themes (WCAG AA minimum).
- Scoping to a specific element (not body) enables side-by-side theme previews without iframes.Why this matters in 2026
The data-theme attribute pattern has become the de-facto standard for multi-theme systems, adopted by Radix UI, shadcn/ui, and most modern design systems precisely because it scopes naturally to any DOM subtree. Unlike class-based theming, attribute selectors don't require specificity juggling, and unlike prefers-color-scheme alone, they allow user-controlled switching without media query overrides. A component library built on semantic tokens can support unlimited themes without touching component CSS.
The logic
CSS custom properties inherit through the DOM, so setting --t-accent on a [data-theme] ancestor makes it available to all descendants without prop-drilling. The component only ever references var(--t-text) and var(--t-card) — it has no knowledge of what those resolve to. Scoping token definitions to an attribute selector (rather than :root) means you can place two data-theme wrappers side by side and get genuinely isolated themes, as shown in the demo above.
Accessibility & performance
Each theme's token values should be validated for WCAG AA contrast (4.5:1 for body text, 3:1 for large text) — the oklch color space makes it straightforward to reason about relative lightness. Theme switching via dataset.theme triggers a single style recalculation with no layout reflow, making it essentially free at runtime. Prefer transition: background-color 0.2s, color 0.2s on root elements for a smooth switch rather than an instant flash.