Docs
Systhema Design (opens in new tab)
Unreleased

Custom tokens

Override or extend tokens from `systhema.config` without editing the exported JSON.

On this page

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

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 shapeLink to this section

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.

// ✗ 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 stylesLink to this section

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:

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 unitLink to this section

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

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 tokensLink to this section

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 overrideLink to this section

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):

import { getTokens } from '@systhemaui/core'

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