Skip to content

theme playground

One block of tokens. Every component.

Nothing below overrides a component selector. Each theme sets a handful of custom properties on the panel, and the kit re-draws itself — the same way it will in your app.

Theme

Notification settings

We'll only use this for account notifications.

Send me
Saves without leaving the page

Delete this account?

This dialog paints in the top layer, outside the panel — but custom properties inherit down the DOM tree, not the paint tree, so it is wearing the theme too.

what changed

/* Nothing overridden. This is packages/core/tokens.css as it ships. */
:root {
  --sk-color-accent: light-dark(oklch(55% 0.24 25), oklch(70% 0.22 25));
  --sk-color-surface: light-dark(oklch(98% 0 0), oklch(20% 0 0));
  --sk-color-surface-raised: light-dark(oklch(100% 0 0), oklch(25% 0 0));
  --sk-color-border: light-dark(oklch(20% 0 0), oklch(88% 0 0));
  --sk-color-border-strong: light-dark(oklch(12% 0 0), oklch(96% 0 0));
  --sk-border-width: 2px;
  --sk-radius-xs: 0;
  --sk-radius-sm: 0;
  --sk-radius-md: 0;
  --sk-radius-lg: 0;
  --sk-radius-xl: 0;
  --sk-font-sans: ui-monospace, "SF Mono", "Cascadia Mono", monospace;
  --sk-tracking-normal: 0.02em;
  --sk-shadow-sm: none;
  --sk-shadow-md: none;
  --sk-shadow-lg: none;
}
:root {
  --sk-color-bg: light-dark(oklch(97.5% 0.012 85), oklch(19% 0.012 85));
  --sk-color-surface: light-dark(oklch(95.5% 0.015 85), oklch(23% 0.013 85));
  --sk-color-surface-raised: light-dark(oklch(99% 0.008 85), oklch(27% 0.014 85));
  --sk-color-text: light-dark(oklch(24% 0.02 60), oklch(94% 0.008 85));
  --sk-color-text-muted: light-dark(oklch(45% 0.02 60), oklch(76% 0.012 85));
  --sk-color-border: light-dark(oklch(86% 0.014 85), oklch(33% 0.014 85));
  --sk-color-accent: light-dark(oklch(44% 0.13 30), oklch(76% 0.11 40));
  --sk-font-sans: Georgia, "Iowan Old Style", "Times New Roman", serif;
  --sk-radius-md: 0.125rem;
  --sk-space-md: 1.25rem;
  --sk-space-lg: 2rem;
  --sk-leading-normal: 1.7;
}
:root {
  color-scheme: dark;
  --sk-color-bg: oklch(15% 0.03 285);
  --sk-color-surface: oklch(19% 0.04 285);
  --sk-color-surface-raised: oklch(23% 0.05 285);
  --sk-color-text: oklch(96% 0.02 285);
  --sk-color-text-muted: oklch(78% 0.04 285);
  --sk-color-border: oklch(34% 0.07 285);
  --sk-color-accent: oklch(84% 0.19 155);
  --sk-radius-md: 0.75rem;
  --sk-shadow-md: 0 0 0 1px oklch(84% 0.19 155 / 0.35), 0 0 24px oklch(84% 0.19 155 / 0.3);
  --sk-shadow-lg: 0 0 0 1px oklch(84% 0.19 155 / 0.4), 0 0 48px oklch(84% 0.19 155 / 0.4);
}

Component selectors touched: none. Every component reads these tokens and nothing else.

How it works

The switcher is a real radio group. One rule per theme re-declares tokens on the panel when its radio is checked — a single :has() condition, no script, and no state held anywhere but the form control.

The re-skin animates because the tokens it moves are registered with @property. An unregistered custom property is an untyped token stream and would snap between values instead of interpolating.

Without :has() support, the default theme renders and the radios have no effect. Under prefers-reduced-motion: reduce, themes change instantly.