Snippets / Layout /

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.

Card Title
Supporting text
Action
Light
Card Title
Supporting text
Action
Dark
Card Title
Supporting text
Action
Brand
Widely Supported
layoutno-js

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.