---
title: "Custom tokens"
description: "Override or extend tokens from `systhema.config` without editing the exported JSON."
url: https://docs.systhema.app/next/design/custom-tokens
version: unreleased (main)
docs_index: https://docs.systhema.app/next/llms.txt
---

Override or extend design tokens from `systhema.config` with `customTokens`, without editing the exported JSON files.

```ts
const config: SysthemaConfig = {
  customTokens: {
    responsiveSizing: {
      xl: {
        grid: { default: { count: '24', gap: '20px', margin: '142px' } },
        container: { width: '1156px' },
      },
    },
    // font, colorPrimitives, colorSystem, textStyles also supported
  },
}
```

## Write the runtime shape

Write the runtime token shape, not the token-file shape. Overrides are deep-merged into the resolved token objects — exactly what `getTokens()` returns. That is _not_ the layout of `src/tokens/*.tokens.json`, which wraps everything in a mode key (`value`) plus a collection root key, and stores each leaf as a DTCG node (`{ $type, $value }`). Mirroring the file is the natural guess and it fails **silently**: the merge just grafts on a branch nothing reads.

```ts
// ✗ WRONG — mirrors font.value.tokens.json (mode wrapper + root key + DTCG node).
customTokens: {
  font: { value: { font: { default: { body: { fontFamily: { $value: 'Inter' } } } } } },
}

// ✓ RIGHT — the runtime path, with a plain value.
customTokens: {
  font: { default: { body: { fontFamily: 'Inter' } } },
}
```

The same rule applies to every collection: `colorPrimitives.theme[500]`, `colorSystem.default.typography.body`, `responsiveSizing.lg.container.width`. Only `colorSystem` and `responsiveSizing` are mode-keyed at their first level (colour mode / breakpoint scope) — `font` and `colorPrimitives` are flattened, so they never take a `value` key.

In development, `@systhemaui/core` warns once per override branch that carries the token-FILE shape — a `$value`/`$type` DTCG node, or a real token path buried under an extra wrapper — and suggests the runtime path. The check is skipped entirely when `NODE_ENV === 'production'`.

## Text styles

`textStyles` can override a shipped composite or add an author-defined one. Each terminal composite becomes a kebab-cased utility class, and token aliases resolve to the corresponding CSS variables:

```ts
customTokens: {
  font: {
    display: { fontFamily: 'Montserrat', fontWeight: '900' },
  },
  responsiveSizing: {
    lg: { typography: { display: { fontSize: '72px', lineHeight: '68px', letterSpacing: '-1px' } } },
  },
  textStyles: {
    'Display Hero': {
      fontFamily: '{font.display.fontFamily}',
      fontWeight: '{font.display.fontWeight}',
      fontSize: '{typography.display.fontSize}',
      lineHeight: '{typography.display.lineHeight}',
      letterSpacing: '{typography.display.letterSpacing}',
      textTransform: 'uppercase',
    },
  },
}
```

This generates `.text-display-hero`. Add the corresponding `responsiveSizing` leaf for every breakpoint where the style should change; Systhema's normal responsive CSS-variable cascade handles the rest.

## Length overrides need a unit

A `responsiveSizing` value is only rewritten into the configured spacing function when it is a **string ending in `px`**. A bare `24` — number _or_ string — matches nothing and reaches the CSS variable verbatim, where it is an invalid length: the browser drops every declaration that substitutes it, with no build error and no console message, and a `var(--x, fallback)` fallback cannot rescue it (a fallback covers an _undefined_ variable, not an invalid substituted value).

```ts
customTokens: {
  responsiveSizing: {
    lg: { header: { logo: { height: '48px' } } }, // ✓ rewritten, emits a length
    // lg: { header: { logo: { height: 48 } } },  // ✗ invalid — the rule is dropped
  },
}
```

In development this warns once per offending path. Only a bare number written over a **px-length** token is reported, so the collection's legitimately unitless tokens (grid counts, font weights, unitless line heights) never trip it.

A `customTokens` leaf may legitimately be a bare `0`, since unitless zero is a valid CSS length. Any other numeric length needs a `px` suffix.

## Extending tokens

Extending the tokens never warns. A new colour mode, a new breakpoint scope, an extra `gap`/`button` variant, an extra entry in a flat colour map, and a brand-new group of your own (`colorSystem.default.pricingTable.headerBg`) are all legitimate — the flatteners walk the merged tree with no allowlist, so they emit real CSS variables (`--color-pricingTable-headerBg`).

## Verifying an override

Verify an override with `getTokens()`, not the artifact files. `.systhema/artifacts/tokens/*.json` are the _inputs_ to token processing — the copies of the source JSON. `customTokens` is merged after they are read, so they always show the pre-merge values and a working override looks broken there. The `.systhema/references/tokens/` TOON files are generated from those same artifacts, so they don't reflect overrides either. Check the merged result instead (or the generated CSS variable):

```ts
import { getTokens } from '@systhemaui/core'

console.log(getTokens().font.default.body.fontFamily)
```
