

# Customize

The primary way to restyle var-ui is a **recipe-shaped override**: call
`theme.componentStyles(recipe, override)` after `createDesignTheme`. Overrides use
`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

```ts
import { badge, button, 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 recipe: `(t) => override` or a plain object. `t` is built-ins + extend refs.
// Style blocks get CSS property IntelliSense; variant keys come from the recipe.
acme.componentStyles(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' },
    },
  ],
}));

acme.componentStyles(badge, {
  base: { borderRadius: '999px' },
});

// Apply
<DesignSystemProvider customTheme={acme}>{/* app */}</DesignSystemProvider>
```

- `acme.className` — theme surface class (`theme-var-ui-acme`)
- `acme.tokens.brand.glow` — typed `var(--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:

```ts
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).

```ts
// 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 { button } from '@var-ui/core';
import type { OverrideConfigFor } from '@var-ui/core';
import type { AcmeTokens } from './acme.extend';

export const buttonOverride = (t: AcmeTokens) =>
  ({
    base: { boxShadow: t.brand.glow },
  }) satisfies OverrideConfigFor<typeof button>;

// theme.ts
import { button, createDesignTheme } from '@var-ui/core';
import { acmeExtend } from './acme.extend';
import { buttonOverride } from './button';

export const acme = createDesignTheme({
  name: 'acme',
  extend: acmeExtend,
});

acme.componentStyles(button, buttonOverride);
```

## Composable primitives

`theme.componentStyles(recipe, override)` compiles to TypeStyles `styles.override` under
the theme class. For app-global or ad-hoc restyles, call `styles.override`
directly:

```ts
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 componentStyles 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                  | `extend` + `componentStyles` (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

`componentStyles` 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.
