Public Surface
Tokens exposed as the public theming API, kept small and semantic.
Color
Intent tokens are used by .primary, .secondary, .success, .warning, .danger, and .neutral. They are curated color/foreground pairs. Actual CSS does not compute foregrounds automatically in the core theme contract.
CSS
:root {
--primary: hsl(270 24% 32%);
--primary-fg: hsl(0 0% 98%);
--secondary: hsl(18 24% 36%);
--secondary-fg: hsl(0 0% 98%);
--success: hsl(145 55% 32%);
--success-fg: hsl(0 0% 98%);
--warning: hsl(38 76% 34%);
--warning-fg: hsl(0 0% 98%);
--danger: hsl(2 62% 45%);
--danger-fg: hsl(0 0% 98%);
--neutral: hsl(220 8% 42%);
--neutral-fg: hsl(0 0% 98%);
}
Surface tokens describe the canvas and elevation model. Components should prefer these over hard-coded colors.
CSS
:root {
--surface: hsl(0 0% 100%);
--surface-raised: hsl(0 0% 100%);
--surface-subtle: hsl(220 14% 96%);
--surface-solid: hsl(222 18% 12%);
--text: hsl(222 18% 12%);
--text-muted: hsl(220 8% 42%);
--text-subtle: hsl(220 7% 58%);
--border: hsl(220 12% 88%);
--focus: var(--primary);
--focus-outline: 2px solid var(--focus);
--focus-outline-offset: 2px;
--focus-ring: hsl(258 21% 32% / 0.22);
--hover-overlay: hsl(220 20% 10% / 0.04);
}
The surface levels are: surface (canvas), surface-raised (cards, raised surfaces), surface-subtle (the working subtle level — for hover overlays, form fields, table headers), and surface-solid (the dark inverse surface, used for accent areas). Use *-fg pairs only for solid backgrounds where the component controls both foreground and background. Do not require a foreground token for every surface token. Focus styling is outline-first. A global focus baseline uses --focus-outline and --focus-outline-offset for the guaranteed visible affordance: older browsers show it on :focus, while modern browsers limit it to :focus-visible. Components add --focus-ring-shadow only where a softer halo helps. forced-colors: active disables the shadow and maps the outline to Highlight.
Default theme philosophy
The default theme is intentionally calm. primary is a near-neutral dark grey rather than a saturated brand color, secondary is a neutral surface rather than a colored intent, and the semantic states are desaturated. Optional themes can re-introduce vivid intent palettes on top of this foundation; the default is meant to be the adult, editorial baseline. Soft surfaces (.btn.soft, .badge.soft, and the default .alert) are generated from intent colors through color-mix(). Themes can tune the mix globally instead of rewriting component selectors.
CSS
:root {
--soft-bg-mix: 88%;
--soft-border-mix: 65%;
--soft-hover-alpha: 12%;
}
Shape
Shape is intentionally small. Components can derive local radii from these.
CSS
:root {
--radius-sm: 0.25rem;
--radius: 0.5rem;
--radius-lg: 0.75rem;
--border-width: 1px;
}
Do not create shape APIs like .rounded, .pill, or .square for every component. Prefer theme-level changes, for example a square theme can set all radius tokens to 0.
Spacing
Spacing should be useful without becoming a design-token encyclopedia.
CSS
:root {
--gap: 0.75rem;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.5rem;
--space-6: 2rem;
}
Use em for spacing that should scale with the component text size, such as button gaps and inline padding. Use rem for global rhythm, layouts, and fixed density.
Typography
Typography tokens should cover the document baseline and common component needs.
CSS
:root {
--font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
--line-height: 1.5;
--font-weight: 400;
--font-weight-strong: 650;
/* Type scale */
--font-size-xs: 0.8125rem;
--font-size-sm: 0.875rem;
--font-size-md: 1rem; /* base */
--font-size-lg: 1.125rem;
}
Components can use local font variables for their own mapping, but should inherit by default. --font-size-md is the baseline — going smaller (.sm/--font-size-sm/-xs) is the easy direction; going larger adjusts line-heights and surrounding paddings, so prefer keeping base body text at --font-size-md.
Controls
Controls share sizing so buttons, inputs, selects, and compact app UI always align. .sm/.lg set shared density tokens for controls, content components, and compact labels.
CSS
:root {
--control-size-sm: 2rem; /* 32px → .sm */
--control-size-md: 2.375rem; /* 38px → default */
--control-size-lg: 2.75rem; /* 44px → .lg */
--control-size: var(--control-size-md);
--control-pad-x: 1em;
--control-font-size: var(--font-size-md);
}
.lg— 44px control height,--font-size-lg(18px)- default — 38px control height,
--font-size-md(16px, the baseline) .sm— 32px control height,--font-size-sm(14px)
Size modifiers swap the shared control tokens:
CSS
.sm {
--control-size: var(--control-size-sm);
--control-font-size: var(--font-size-sm);
--variant-font-size: var(--font-size-sm);
--variant-icon-size: 1rem;
--variant-pad-block: var(--space-3);
--variant-compact-size: 1.25rem;
--variant-compact-font-size: 0.75rem;
}
.lg {
--control-size: var(--control-size-lg);
--control-font-size: var(--font-size-lg);
--variant-font-size: var(--font-size-lg);
--variant-icon-size: 1.5rem;
--variant-pad-block: var(--space-5);
--variant-compact-size: 1.875rem;
--variant-compact-font-size: var(--font-size-sm);
}
Content components can consume --variant- for local density. Compact labels such as .badge consume --variant-compact-; display elements with special geometry, such as avatar and spinner, keep their own size scales.
Elevation
Use a tiny elevation surface. Components should not invent their own shadow scales.
CSS
:root {
--shadow: 0 0.5rem 1.5rem hsl(220 30% 10% / 0.08);
--shadow-popout: 0 0.25rem 0.75rem hsl(220 30% 10% / 0.15);
}
Motion
Motion tokens keep transitions consistent and easy to disable.
CSS
:root {
--duration: 150ms; /* component transitions (hover/focus) */
--duration-slow: 200ms; /* one-shot open/close (.drawer animation) */
--duration-spin: 0.75s; /* continuous spin (.spinner) */
--duration-shimmer: 1.2s; /* continuous shimmer (.skeleton, indeterminate .progress) */
--ease: ease;
}
@media (prefers-reduced-motion: reduce) {
:root {
--duration: 0.01ms;
}
}
reset.css clamps raw animation and transition durations to 0.01ms, resets their delays, and restores html scroll behavior under prefers-reduced-motion: reduce in addition to the --duration token above. Decorative loops effectively complete immediately without blanket-disabling animation support.
Icons
Icons used by CSS-only controls are public tokens because users may need to replace them.
CSS
:root {
--icon-chevron-mask: url("data:image/svg+xml,...stroke='black'...");
--icon-check-mask: url("data:image/svg+xml,...stroke='black'...");
--icon-chevron-image: url("data:image/svg+xml,...");
}
Prefer *-mask tokens for CSS icons so components can color them with background-color: currentColor or a semantic token. Use image tokens only where the platform requires background-image (native select chevrons cannot use currentColor). Only add an icon token when CSS needs the icon. Markup icons should stay in markup.
Composition tokens
A few cross-component helpers:
--indicator-offset/--indicator-ring— positioning and contrast ring for a status dot attached to.avatar > .badge:empty.--backdrop-color/--backdrop-opacity/--backdrop-fill— scrim color and opacity shared bydialog.modal::backdrop,dialog.drawer::backdrop, and.surface-backdrop. Override--backdrop-opacityper theme for a denser or lighter scrim.
CSS
:root {
--indicator-offset: 14.65%; /* circle-aware inset */
--indicator-ring: var(--surface);
--backdrop-color: var(--surface-solid);
--backdrop-opacity: 0.45;
}