Customize
The primary way to restyle var-ui is a recipe-shaped theme config: override
base / variants / compoundVariants with full CSS properties, autocomplete
variant names from the recipe, and mint custom tokens that participate in light
and dark mode — with no class names in your theme code.
Cascade layers keep overrides winning without specificity fights: recipes live
in components, consumer overrides in overrides.
One theme call
import { createDesignTheme } from '@var-ui/core';
import { DesignSystemProvider } from '@var-ui/react';
export const acme = createDesignTheme({
name: 'acme',
// Partial — deep-merged onto the built-in default palette
tokens: {
color: {
accent: {
default: 'oklch(55% 0.2 290)',
hover: 'oklch(48% 0.2 290)',
},
},
},
colorMode: {
dark: {
accent: {
default: 'oklch(72% 0.16 290)',
hover: 'oklch(78% 0.14 290)',
},
},
},
// Custom tokens — string leaves are mode-invariant; { light, dark } follow color mode
extend: {
brand: {
glow: {
light: '0 0 0 3px oklch(90% 0.08 290)',
dark: '0 0 16px oklch(70% 0.18 290)',
},
},
},
// Per-key: `(t) => override` or a plain object. `t` is built-ins + extend refs.
// Style blocks get CSS property IntelliSense; variant keys come from the recipe.
components: {
button: (t) => ({
base: {
borderRadius: '999px',
boxShadow: t.brand.glow,
'&:hover': { boxShadow: 'none' },
},
variants: {
tone: {
// keys autocomplete: neutral | accent | success | warning | danger | info
accent: { textTransform: 'uppercase' },
},
appearance: {
filled: {},
},
},
compoundVariants: [
{
variants: { tone: 'accent', appearance: 'filled', size: 'lg' },
style: { letterSpacing: '0.08em' },
},
],
}),
badge: {
base: { borderRadius: '999px' },
},
},
});
// Apply
<DesignSystemProvider customTheme={acme}>{/* app */}</DesignSystemProvider>
acme.className— theme surface class (theme-var-ui-acme)acme.tokens.brand.glow— typedvar(--var-ui-brand-glow)ref
Try the Acme (V7 + V8 demo) palette in the vite example app to see this live.
Self-hosted fonts
Register @font-face rules with fonts on createDesignTheme (or on a
from preset). Pair with tokens.fontFamily stacks that reference the same
family names. Use defineFonts to generate stacks from face definitions:
import { createDesignTheme, defineFonts, groteskMono } from '@var-ui/core';
const brand = defineFonts({
body: {
face: {
family: 'Acme Sans',
src: "url('/fonts/acme-sans.woff2') format('woff2')",
fontWeight: '400 700',
fontDisplay: 'swap',
},
fallback: 'system-ui, sans-serif',
},
});
export const acme = createDesignTheme({
name: 'acme',
fonts: brand.fonts,
tokens: { fontFamily: brand.tokens.fontFamily },
});
Host font files under your app’s public/fonts/ directory. Root-relative
url('/fonts/…') works with Astro, Vite, and Next static serving.
Split across files
Hoist extend into a leaf module and type factories with DesignThemeTokens —
avoid typeof theme.tokens in override files (that cycles through the theme).
// acme.extend.ts
import type { DesignThemeTokens } from '@var-ui/core';
export const acmeExtend = {
brand: {
glow: { light: '…', dark: '…' },
},
} as const;
export type AcmeTokens = DesignThemeTokens<typeof acmeExtend>;
// button.ts
import type { AcmeTokens } from './acme.extend';
export const button = (t: AcmeTokens) => ({
base: { boxShadow: t.brand.glow },
});
// theme.ts
import { createDesignTheme } from '@var-ui/core';
import { acmeExtend } from './acme.extend';
import { button } from './button';
createDesignTheme({
name: 'acme',
extend: acmeExtend,
components: { button },
});
Composable primitives
createDesignTheme({ components }) compiles to TypeStyles styles.override under
the theme class. For app-global or ad-hoc restyles, call styles.override
directly:
import { styles, extendTokens, button } from '@var-ui/core';
// App-global restyle — no theme class prefix
styles.override(button, { base: { borderRadius: '999px' } }, { layer: 'overrides' });
// Theme-scoped (same mechanism createDesignTheme uses internally)
styles.override(
button,
{ variants: { tone: { danger: { fontWeight: 700 } }, appearance: { filled: {} } } },
{ selectorPrefix: `.${acme.className}`, layer: 'overrides' },
);
// Custom tokens without a theme (global :root + dark mode rules)
const brand = extendTokens('brand', {
glow: { light: '…', dark: '…' },
});
When to drop down a tier
| Need | Use |
|---|---|
| Bulk restyle / new brand tokens | components / extend (this page) |
| One property the recipe already exposes as a var | Set the CSS custom property in an override block or stylesheet |
| Nested themes fighting over the same recipe | Prefer component vars (inheritance), or styles.scope() / @scope |
| Hand-written CSS against a public class | Target the stable base/slot class (e.g. .var-ui-button) |
Attribute mode means variant state is data-* on the element
(data-tone="accent" and data-appearance="filled"), not a separate class. Overrides still target the
right selectors via TypeStyles styles.override — you never write those
selectors yourself in the theme config.
Nested themes
components overrides use a descendant selector prefix
(.theme-var-ui-acme .var-ui-button). If two themed regions nest and both override the
same recipe, prefer component CSS variables (Tier 1) or styles.scope() for
proximity-correct CSS. See TypeStyles’ component override contract for details.