End-to-end theming
Build and apply a TypeStyles theme — tokens, createTheme, override, and mount
A complete theming setup with TypeStyles: design tokens, a theme surface class, recipe overrides, and mounting in the DOM. Consumers can customize with TypeStyles APIs or plain CSS that targets the same class names and custom properties.
For more patterns (multi-brand, condition scopes, @property animation), see Theming patterns. Coming from StyleX / Panda / vanilla-extract / Stitches? See Migration.
What you get
| Piece | CSS / DOM |
|---|---|
tokens.create / declare |
:root { --app-color-… } |
tokens.createTheme({ name, … }) |
.theme-app-brand { --app-color-… } (+ optional light-dark(), @media, attrs) |
recipe |
Readable recipe classes (button--intent-primary) |
override(…, { selectorPrefix }) |
.theme-app-brand .button { … } |
| Plain CSS | Same class and --* names — consumers can theme without TypeStyles |
Walkthrough
Defaults live on :root via tokens.create. Themes only set what changes under a theme class — brand accents, theme-local namespaces, recipe overrides. Unchanged paths inherit through the cascade.
Light and dark belong on the token leaves ({ light, dark }) when you register colorModes — not as parallel untyped constants. Passing { decl } types those values against the schema.
1. Shared runtime
// typestyles.ts
import { colorModes, createTypeStyles } from 'typestyles';
export const { recipe, tokens, override } = createTypeStyles({
scopeId: 'app',
colorModes,
layers: {
order: ['tokens', 'components', 'overrides'] as const,
token: 'tokens',
style: 'components',
},
});
2. Color tokens
// tokens/color.ts
import { atProperty } from 'typestyles';
import { tokens } from '../typestyles';
const colorDecl = tokens.declare('color', {
text: atProperty.color,
surface: atProperty.color,
accent: { default: atProperty.color },
});
/** `:root` defaults — mode-aware leaves compile to `light-dark()` when `colorModes` is set. */
export const color = tokens.create(
'color',
{
text: { light: '#111827', dark: '#e5e7eb' },
surface: { light: '#ffffff', dark: '#0f172a' },
accent: { default: { light: '#2563eb', dark: '#60a5fa' } },
},
{ decl: colorDecl },
);
atProperty.color is { syntax: '<color>', initial: 'transparent' } — spread or override per leaf ({ ...atProperty.color, inherits: false }). Same presets exist for length, time, angle, and more; see CSS primitives — atProperty presets.
color.text / color.accent.default are typed var(--…) refs (syntax-branded). Use them in recipes — do not feed them back into createTheme({ tokens }) as values.
3. Button recipe
// components/button.ts
import { recipe } from '../typestyles';
import { color } from '../tokens/color';
export const button = recipe(
'button',
{
base: {
padding: '8px 16px',
borderRadius: '6px',
border: 'none',
cursor: 'pointer',
backgroundColor: color.accent.default,
color: color.text,
},
variants: {
intent: {
primary: {},
ghost: {
backgroundColor: 'transparent',
color: color.accent.default,
border: `1px solid ${color.accent.default}`,
},
},
},
defaultVariants: { intent: 'primary' },
},
{ layer: 'components' },
);
4. Brand theme
// themes/brand.ts
import { tokens } from '../typestyles';
// Recipe must be registered before createTheme({ components })
import '../components/button';
/**
* Theme-class deltas only. Root still supplies text/surface via cascade;
* this surface retints accent and adds a theme-local `brand` namespace.
*/
export const brand = tokens.createTheme({
name: 'brand',
tokens: {
color: {
accent: { default: { light: '#7c3aed', dark: '#a78bfa' } },
},
brand: {
glow: { default: '#a78bfa' },
},
},
// Same as override(button, …, { selectorPrefix: `.${brand.className}` })
components: {
button: ({ tokens: t }) => ({
base: { boxShadow: `0 0 0 3px ${t.brand.glow.default}` },
}),
},
});
brand.className is something like theme-app-brand. brand.tokens.brand.glow.default is a var(--…) string — the same shape as tokens.use('color'). Define additional themes the same way and switch by swapping className on a parent — see Multi-brand theming.
Side-effect-import these modules from your typestyles entry so extraction sees every registration.
Optional: override without components
// themes/brand-overrides.ts
import { override } from '../typestyles';
import { button } from '../components/button';
import { brand } from './brand';
override(
button,
{ variants: { intent: { ghost: { textDecoration: 'underline' } } } },
{ selectorPrefix: `.${brand.className}`, layer: 'overrides' },
);
Sharing one root theme across many brands? Use Theme.override.
Mount (React)
Root mode-aware tokens compile to light-dark(). With colorModes configured, createTheme also emits color-scheme: light dark on the theme class. For an explicit data-mode toggle, see condition scopes or attribute-driven modes.
// App.tsx
import { button } from './components/button';
import { brand } from './themes/brand';
export function App() {
return (
<div className={brand.className}>
<button type="button" className={button()}>
Primary
</button>
<button type="button" className={button({ intent: 'ghost' })}>
Ghost
</button>
</div>
);
}
Mount (HTML)
<div class="theme-app-brand">
<button class="button button--intent-primary">Primary</button>
</div>
Or override tokens from plain CSS (no TypeStyles in the consumer app):
.theme-app-brand {
--app-color-accent-default: #059669;
}
.theme-app-brand .button--intent-primary {
border-radius: 999px;
}
Selector prefix convention
Theme-scoped recipe restyles use a descendant prefix (not CSS @scope):
override(
button,
{ base: { borderRadius: '999px' } },
{
selectorPrefix: `.${brand.className}`, // → ".theme-app-brand .button { … }"
layer: 'overrides',
},
);
Prefer this (or createTheme({ components })) over hand-written class strings. Cascade layers keep override order predictable — see Cascade layers.
Related APIs
| Need | API |
|---|---|
| Deep-merge override trees / leaf refs | mergeThemeOverrides |
| Fork a root theme for brands / apps | Theme.override |
| Media / attr / class conditions | tokens.when, tokens.colorMode.* |
| Zero-runtime extract of all recipes | getRegisteredComponentRefs + Vite extract.registeredComponentsModule |
Checklist for design-system authors
- Export a shared
createTypeStyles({ scopeId, colorModes })runtime. tokens.declarethe schema, thentokens.create(…, { decl })with mode-aware{ light, dark }leaves for defaults.- Register recipes with
recipeon that same runtime. - Document that apps call
tokens.createTheme({ name, tokens, … })with overrides only, then applytheme.className. - Document
selectorPrefix: \.${theme.className}`(or themecomponents`) for recipe restyles. - Keep class and
--*names stable so consumers can also theme from plain CSS.