Theming
One stylesheet of CSS custom properties drives every component — colors, control sizes, radii and motion.
Theming is CSS custom properties, nothing else. No provider, no JS config, no build step. Override a variable and every component that reads it follows, instantly, including ones you copied months ago.
Light and dark
The theme is an attribute on <html>:
<html data-theme="dark"> <!-- default -->
<html data-theme="light">
tokens.css defines the dark palette on :root and overrides it under
:root[data-theme='light']. Switching is one attribute write:
document.documentElement.dataset.theme = 'light';
localStorage.setItem('theme', 'light');
Set it before first paint (an inline script in <head>) so the page never
flashes the wrong palette.
The token groups
Surfaces and text
| Token | Role |
|---|---|
--bg | Page background |
--bg-card | Card / panel surface |
--bg-elevated | Popovers, menus, dialogs |
--bg-input | Field interiors |
--border | Default hairline |
--border-hover · --border-strong | Hover and emphasis |
--text | Primary text |
--text-secondary | Body copy |
--text-muted | Labels, hints, captions |
Accent and status
--accent and --accent-contrast drive the primary action color.
--success, --danger, --warn and --info back the tone prop that most
components accept.
:root {
--accent: #7c5cff;
--accent-contrast: #ffffff;
}
That single change repaints every primary button, focus ring and active state in the app.
The control scale
This is the one that keeps a row of mixed components aligned. Height, radius, font size and horizontal padding all come from the same three-step scale:
| sm | md | lg | |
|---|---|---|---|
| Height | --ctrl-h-sm 32px | --ctrl-h-md 40px | --ctrl-h-lg 48px |
| Radius | --ctrl-r-sm 10px | --ctrl-r-md 12px | --ctrl-r-lg 14px |
| Font | --ctrl-fs-sm 13px | --ctrl-fs-md 14px | --ctrl-fs-lg 15px |
| Padding X | --ctrl-px-sm 12px | --ctrl-px-md 14px | --ctrl-px-lg 18px |
A button, an input and a select with size="md" line up pixel for pixel
because all three read --ctrl-h-md. Want a squarer product? Change the three
radii once:
:root {
--ctrl-r-sm: 4px;
--ctrl-r-md: 6px;
--ctrl-r-lg: 8px;
}
Motion
| Token | Default |
|---|---|
--dur-fast | 200ms |
--dur-mid | 260ms |
--dur-slow | 320ms |
--ease-out | cubic-bezier(.22, 1, .36, 1) |
--ease-spring | cubic-bezier(.34, 1.56, .64, 1) |
Under prefers-reduced-motion: reduce, tokens.css zeroes the durations for
you. JS-driven motion has its own guard — see
Effects & motion.
Effects tint
--fx-tint is the light color used by the proximity glow and the press
ripple, written as space-separated RGB because the effects compose it with
alpha:
:root { --fx-tint: 255 255 255; } /* dark theme */
:root[data-theme='light'] { --fx-tint: 0 0 0; } /* light theme */
Scoping a theme to one subtree
Tokens cascade, so a section can carry its own palette:
<section style="--accent: #22c55e; --bg-card: #04140b;">
<VsButton label="Deploy" />
</section>
Nothing else in the page changes.
Per-component tokens
Some components expose their own namespaced variables for fine control —
--btn-primary-bg, --inp-border, --sel-*, and so on. They all fall back to
the global tokens, so you only reach for them when you want that one component
to differ:
.checkout .btn--primary {
--btn-primary-bg: #111;
--btn-primary-fg: #fff;
}