> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vertz.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Styling

> Scoped styles with css() and parameterized styles with variants()

Vertz provides a built-in styling system based on design tokens. No CSS files, no class name collisions, no build-step CSS tooling. You write styles in TypeScript and get scoped class names at runtime.

## `css()` -- scoped styles

Create a group of scoped styles with `css()` and the `token` helper:

```tsx theme={null}
import { css, token } from '@vertz/ui';

const styles = css({
  card: {
    backgroundColor: token.color.card,
    borderRadius: token.radius.lg,
    padding: token.spacing[4],
    borderWidth: '1px',
    borderColor: token.color.border,
  },
  title: {
    fontSize: token.font.size.lg,
    fontWeight: token.font.weight.bold,
    color: token.color.foreground,
  },
  meta: {
    fontSize: token.font.size.sm,
    color: token.color['muted-foreground'],
  },
});

export function TaskCard({ task }: TaskCardProps) {
  return (
    <div className={styles.card}>
      <h3 className={styles.title}>{task.title}</h3>
      <span className={styles.meta}>Created {task.createdAt}</span>
    </div>
  );
}
```

Each key in the object becomes a scoped class name. CSS property names use camelCase (`backgroundColor`, `borderRadius`). The `token` helper resolves to CSS custom properties from your theme.

### Token helper

The `token` namespace is typed against your active theme. Use dot notation for camelCase token names and bracket notation for any token name that contains a hyphen or a shade number.

| Expression                        | CSS output                      |
| --------------------------------- | ------------------------------- |
| `token.color.card`                | `var(--color-card)`             |
| `token.color.foreground`          | `var(--color-foreground)`       |
| `token.color['muted-foreground']` | `var(--color-muted-foreground)` |
| `token.color.primary[600]`        | `var(--color-primary-600)`      |
| `token.spacing[4]`                | `var(--spacing-4)`              |
| `token.font.size.lg`              | `var(--font-size-lg)`           |
| `token.font.weight.bold`          | `var(--font-weight-bold)`       |
| `token.radius.lg`                 | `var(--radius-lg)`              |

Raw CSS keywords like `'flex'`, `'center'`, `'100%'`, `'inherit'`, or `'transparent'` are passed through literally.

```tsx theme={null}
const styles = css({
  row: {
    display: 'flex',
    alignItems: 'center',
    gap: token.spacing[2],
  },
});
```

The full token set comes from your theme (`@vertz/theme-shadcn` or a custom theme defined with `defineTheme()`).

### Shade color tokens

Color palettes support a full shade scale beyond the semantic tokens. Use bracket notation for the numeric shade:

```tsx theme={null}
const styles = css({
  heading: { color: token.color.gray[900] },
  subtitle: { color: token.color.zinc[500] },
  card: {
    backgroundColor: token.color.gray[50],
    borderWidth: '1px',
    borderColor: token.color.gray[200],
  },
  accent: {
    backgroundColor: token.color.blue[600],
    color: 'white',
  },
  warning: {
    backgroundColor: token.color.amber[100],
    color: token.color.amber[800],
    borderWidth: '1px',
    borderColor: token.color.amber[300],
  },
});
```

**Available namespaces** -- these are the color namespaces that support shade notation:

| Namespace     | Example                        | CSS output                                       |
| ------------- | ------------------------------ | ------------------------------------------------ |
| `primary`     | `token.color.primary[600]`     | `background-color: var(--color-primary-600)`     |
| `secondary`   | `token.color.secondary[400]`   | `color: var(--color-secondary-400)`              |
| `accent`      | `token.color.accent[100]`      | `background-color: var(--color-accent-100)`      |
| `gray`        | `token.color.gray[500]`        | `color: var(--color-gray-500)`                   |
| `destructive` | `token.color.destructive[600]` | `background-color: var(--color-destructive-600)` |
| `danger`      | `token.color.danger[500]`      | `color: var(--color-danger-500)`                 |
| `success`     | `token.color.success[100]`     | `background-color: var(--color-success-100)`     |
| `warning`     | `token.color.warning[300]`     | `border-color: var(--color-warning-300)`         |
| `info`        | `token.color.info[50]`         | `background-color: var(--color-info-50)`         |
| `muted`       | `token.color.muted[600]`       | `color: var(--color-muted-600)`                  |
| `surface`     | `token.color.surface[100]`     | `background-color: var(--color-surface-100)`     |

**Shade scale** -- each namespace supports shades from `50` (lightest) to `950` (darkest):

`50`, `100`, `200`, `300`, `400`, `500`, `600`, `700`, `800`, `900`, `950`

The actual color values are defined by your theme. When using palettes from `@vertz/ui/css`, the Tailwind v4 oklch color palettes are available (slate, gray, zinc, neutral, stone, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose).

**Using palettes in your theme:**

```tsx theme={null}
import { defineTheme } from '@vertz/ui';
import { palettes } from '@vertz/ui/css';

const theme = defineTheme({
  colors: {
    primary: palettes.blue, // all shades 50-950
    danger: palettes.red,
    success: palettes.emerald,
    gray: palettes.zinc,
  },
});
```

### Pseudo-state selectors

Use nested `&:` selectors to apply styles on pseudo-states:

```tsx theme={null}
const styles = css({
  link: {
    color: token.color.primary,
    textDecoration: 'none',
    '&:hover': {
      color: token.color.primary[700],
      textDecoration: 'underline',
    },
  },
  input: {
    borderWidth: '1px',
    borderColor: token.color.border,
    borderRadius: token.radius.md,
    paddingInline: token.spacing[3],
    paddingBlock: token.spacing[2],
    '&:focus': {
      borderColor: token.color.primary,
      boxShadow: '0 0 0 2px var(--color-ring)',
    },
    '&:disabled': {
      opacity: 0.5,
      cursor: 'not-allowed',
    },
  },
});
```

## `variants()` -- parameterized styles

Use `variants()` for components with multiple visual states:

```tsx theme={null}
import { variants, token } from '@vertz/ui';

const button = variants({
  base: {
    display: 'inline-flex',
    alignItems: 'center',
    borderRadius: token.radius.md,
    fontWeight: token.font.weight.medium,
    transition: 'all 150ms ease',
  },
  variants: {
    intent: {
      primary: {
        backgroundColor: token.color.primary,
        color: token.color['primary-foreground'],
      },
      secondary: {
        backgroundColor: token.color.secondary,
        color: token.color['secondary-foreground'],
      },
      ghost: {
        backgroundColor: 'transparent',
        color: token.color.foreground,
      },
      danger: {
        backgroundColor: token.color.destructive,
        color: 'white',
      },
    },
    size: {
      sm: {
        fontSize: token.font.size.xs,
        paddingInline: token.spacing[3],
        paddingBlock: token.spacing[1],
      },
      md: {
        fontSize: token.font.size.sm,
        paddingInline: token.spacing[4],
        paddingBlock: token.spacing[2],
      },
      lg: {
        fontSize: token.font.size.base,
        paddingInline: token.spacing[6],
        paddingBlock: token.spacing[3],
      },
    },
  },
  defaultVariants: { intent: 'primary', size: 'md' },
});
```

Use it in JSX by calling the function with variant values:

```tsx theme={null}
<button className={button({ intent: 'primary', size: 'lg' })}>
  Save
</button>

<button className={button({ intent: 'ghost', size: 'sm' })}>
  Cancel
</button>

{/* Uses defaultVariants when omitted */}
<button className={button()}>
  Default
</button>
```

### Reactive variants

Variant values can be reactive -- the class name updates automatically when the signal changes:

```tsx theme={null}
let isActive = false;

return (
  <button
    className={button({ intent: isActive ? 'primary' : 'ghost' })}
    onClick={() => {
      isActive = !isActive;
    }}
  >
    Toggle
  </button>
);
```

## When to use `css()` vs inline styles

**Use `css()` for:**

* Layout and spacing (`display: 'flex'`, `padding: token.spacing[4]`, `gap: token.spacing[2]`)
* Sizing (`width: '100%'`, `height: '100vh'`) and aspect ratios (`aspectRatio: '16 / 9'`)
* Positioning (`position: 'absolute'`, `top: 0`, `left: token.spacing[4]`, `inset: 0`)
* Images (`objectFit: 'cover'`, `objectFit: 'contain'`)
* Typography scales (`fontSize: token.font.size.lg`, `fontWeight: token.font.weight.semibold`)
* Borders and radius (`borderWidth: '1px'`, `borderColor: token.color.border`, `borderRadius: token.radius.lg`)
* Colors from the theme/shade system (`backgroundColor: token.color.primary[600]`, `color: token.color.gray[500]`)
* Pseudo-states (`&:hover`, `&:focus`, `&:disabled`)

**Use inline `style` for:**

* Truly dynamic values computed at runtime (`transform`, `translate`)
* Syntax highlight colors or external color values
* Complex CSS that has no token equivalent (gradients, `clamp()`, `backdrop-filter`)
* Custom shadows beyond the shadow scale
* CSS custom property references (`var(--my-custom-prop)`)

```tsx theme={null}
{
  /* css() -- theme-aware, scoped, pseudo-state support */
}
const styles = css({
  card: {
    backgroundColor: token.color.card,
    borderRadius: token.radius.lg,
    padding: token.spacing[6],
    boxShadow: 'var(--shadow-md)',
  },
  title: {
    fontSize: token.font.size.xl,
    fontWeight: token.font.weight.bold,
    color: token.color.foreground,
  },
});

{
  /* inline style -- dynamic runtime value */
}
<div style={{ transform: `translateX(${offset}px)` }}>Sliding content</div>;

{
  /* inline style -- complex CSS without token equivalent */
}
<div style={{ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}>
  Gradient background
</div>;
```

## Theme

Vertz uses CSS custom properties for theming. The `@vertz/theme-shadcn` package provides a shadcn/ui-inspired token set with light and dark mode support.

```tsx theme={null}
import { defineTheme, ThemeProvider } from '@vertz/ui';
import { shadcnTheme } from '@vertz/theme-shadcn';

// Use the built-in theme
ThemeProvider({ theme: shadcnTheme }, () => <App />);
```

### Custom themes

Define your own theme with `defineTheme()`:

```tsx theme={null}
import { defineTheme } from '@vertz/ui';

const myTheme = defineTheme({
  colors: {
    background: '#ffffff',
    foreground: '#0f0f12',
    primary: '#3b82f6',
    // ... other tokens
  },
  dark: {
    background: '#0f0f12',
    foreground: '#fafafa',
    primary: '#60a5fa',
  },
});
```

## Global styles

For reset styles or global base rules, use `globalCss()`:

```tsx theme={null}
import { globalCss, token } from '@vertz/ui';

globalCss({
  '*': {
    boxSizing: 'border-box',
    margin: 0,
    padding: 0,
  },
  body: {
    backgroundColor: token.color.background,
    color: token.color.foreground,
    fontFamily: 'var(--font-sans)',
  },
});
```

Global styles are injected once and aren't scoped -- use sparingly.

## Fonts

Declare font families with `font()` and compile them into `@font-face` CSS, custom properties (`--font-<key>`), and preload tags with `compileFonts()`. Only woff2 format is supported.

```tsx theme={null}
import { font, compileFonts } from '@vertz/ui/css';

const sans = font('DM Sans', {
  weight: '100..1000',
  src: '/fonts/dm-sans.woff2',
  fallback: ['system-ui', 'sans-serif'],
});

const mono = font('JetBrains Mono', {
  weight: '100..800',
  src: '/fonts/jb-mono.woff2',
  fallback: ['monospace'],
});

const compiled = compileFonts({ sans, mono });

// compiled.fontFaceCss  — @font-face declarations
// compiled.cssVarsCss   — :root { --font-sans: ...; --font-mono: ...; }
// compiled.cssVarLines  — individual lines for merging into an existing :root
// compiled.preloadTags  — <link rel="preload" ...> HTML tags
```

Reference the generated CSS vars in components:

```tsx theme={null}
function CodeBlock({ code }: { code: string }) {
  return (
    <pre style={{ fontFamily: 'var(--font-mono)' }}>
      <code>{code}</code>
    </pre>
  );
}
```

### Multiple font files

Pass an array of `src` entries for variants like normal + italic:

```tsx theme={null}
const sans = font('DM Sans', {
  weight: '100..1000',
  src: [
    { path: '/fonts/dm-sans.woff2', weight: '100..1000', style: 'normal' },
    { path: '/fonts/dm-sans-italic.woff2', weight: '100..1000', style: 'italic' },
  ],
  fallback: ['system-ui', 'sans-serif'],
});
```

Each entry generates a separate `@font-face` block. Only the first file is preloaded.

### System fonts (no `src`)

Omit `src` to generate a CSS custom property without an `@font-face` block:

```tsx theme={null}
const system = font('system-ui', { weight: '400' });
// compileFonts({ system }) → --font-system: 'system-ui'; (no @font-face)
```

### Google Fonts

Use `googleFont()` to declare a Google Font by name. The framework automatically fetches the `.woff2` file at dev/build time, caches it locally, and serves it self-hosted. No manual downloads needed.

```tsx theme={null}
import { googleFont } from '@vertz/ui/css';
import { configureTheme } from '@vertz/theme-shadcn';
import { registerTheme } from '@vertz/ui';

const sans = googleFont('Inter', {
  weight: '100..900',
  subsets: ['latin'],
});

const mono = googleFont('JetBrains Mono', {
  weight: '100..800',
  subsets: ['latin'],
});

const config = configureTheme({
  palette: 'zinc',
  fonts: { sans, mono },
});

registerTheme(config);
```

That's the complete setup. The dev server handles everything else:

* Fetches the font from Google Fonts CSS2 API on first startup
* Downloads the `.woff2` file to `.vertz/fonts/` (cached across restarts)
* Generates `@font-face` CSS with the local file path
* Computes zero-CLS fallback metrics automatically
* Injects `<link rel="preload">` tags in SSR

**`googleFont()` options:**

| Option               | Type                                                      | Default       | Description                                                                    |
| -------------------- | --------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------ |
| `weight`             | `string \| number \| number[]`                            | (required)    | Weight range (`'100..900'`), single weight (`400`), or multiple (`[400, 700]`) |
| `style`              | `'normal' \| 'italic' \| ('normal' \| 'italic')[]`        | `'normal'`    | Font style(s)                                                                  |
| `display`            | `'auto' \| 'block' \| 'swap' \| 'fallback' \| 'optional'` | `'swap'`      | `font-display` strategy                                                        |
| `subsets`            | `string[]`                                                | `['latin']`   | Character subsets                                                              |
| `fallback`           | `string[]`                                                | auto-detected | Fallback font stack                                                            |
| `adjustFontFallback` | `boolean`                                                 | `true`        | Enable zero-CLS metric-adjusted fallback                                       |

**Static font with specific weights:**

```tsx theme={null}
const heading = googleFont('Playfair Display', {
  weight: [400, 700],
  style: ['normal', 'italic'],
  subsets: ['latin'],
});
```

**Using `font()` alongside `googleFont()`:**

`font()` and `googleFont()` return the same `FontDescriptor` type and can be mixed freely in themes:

```tsx theme={null}
import { font, googleFont } from '@vertz/ui/css';

const sans = googleFont('Inter', { weight: '100..900' });
const brand = font('Custom Brand', {
  weight: '400..700',
  src: '/fonts/brand.woff2',
  fallback: ['sans-serif'],
});

configureTheme({ fonts: { sans, brand } });
```

<Note>
  Google Fonts are all open-source licensed (SIL OFL or Apache 2.0). Self-hosting is explicitly
  permitted.
</Note>

### Offline and CI caching

Font files are cached in `.vertz/fonts/` and persist across dev server restarts. For CI environments without network access, cache this directory:

```yaml theme={null}
# GitHub Actions example
- uses: actions/cache@v4
  with:
    path: .vertz/fonts
    key: vertz-fonts-${{ hashFiles('src/styles/theme.ts') }}
```

If the cache is empty and no network is available, the dev server logs an error and falls back to system fonts.
