Token Architecture

Design tokens are the visual DNA of your design system. Spektral uses a three-layer architecture based on theDTCG(Design Tokens Community Group) format.

Three Layers

Primitive   raw values: "#1e40af", "16px", "'Inter', sans-serif"
    ↑ referenced by
Semantic    purpose aliases: "$value": "{color.brand-blue.70}"
    ↑ referenced by
Component   component defaults: "$value": "{color.primary}"

Primitive

Raw design values with no implied meaning. The foundation of the entire token system.

color.navy-500: #173a6b
spacing.4: 16px
borderRadius.md: 8px

Semantic

Purpose-based aliases that reference primitives. Changing a semantic token cascades to all components.

color.primary: {color.brand-blue.70}
spacing.md: {spacing.4}
radius.md: {borderRadius.md}

Component

Component-specific defaults that reference semantic tokens. Overridable per-component.

button.bg: {color.primary}
button.radius: {radius.md}
button.padding-x: {spacing.md}

DTCG JSON Format

Every token is a JSON object with $value and $type properties:

// primitive.tokens.json
{
  "color": {
    "brand-blue": {
      "70": {
        "$value": "#1e40af",
        "$type": "color"
      }
    }
  },
  "spacing": {
    "4": {
      "$value": "16px",
      "$type": "dimension"
    }
  }
}

// semantic.tokens.json
{
  "color": {
    "primary": {
      "$value": "{color.brand-blue.70}",
      "$type": "color"
    }
  }
}

Reference Chain

Semantic and component tokens use {curly.brace} syntax to reference tokens in the layer below. References must resolve: a broken reference will fail the Style Dictionary build.

button.bg → {color.primary} → {color.brand-blue.70} → #1e40af

Changing color.brand-blue.70 cascades through semantic and component layers automatically.

Style Dictionary Build

Spektral uses Style Dictionary to transform DTCG JSON tokens into CSS custom properties.

tokens/default/*.tokens.json
    ↓ Style Dictionary
platforms/react/ds-lib/src/tokens/variables.css
platforms/vue/ds-lib/src/tokens/variables.css
platforms/lit/ds-lib/src/tokens/variables.css
... every initialized platform

The same tokens are built into every platform. Components consume them as var(--color-primary) in CSS. Output references are preserved (outputReferences: true) so DevTools shows the token name, not resolved values.

Multi-Brand Support

By default, tokens live in brand directories under tokens/. Each brand has the same three-layer structure with different values. Switching brands rebuilds all platforms with the new token values.

tokens/
  default/     primitive.tokens.json, semantic.tokens.json, component.tokens.json
  spektral/    same structure, different values
  kevla/       same structure, different values
  brands.json   { "active": "default" }

Light and dark are not brands. They are modes, a second axis that varies inside every brand and is switched at runtime rather than at build time. See Add dark mode.

Already have tokens? Keep them where they are

Everything above is Spektral's default layout, not a requirement. If your design system already exists, you do not restructure it to adopt Spektral: you point Spektral at the tree you have.

An optional spektral.yml at your project root covers the differences: tokens in a package subdirectory, a flat tree with no brand folders, layers named something other than primitive/semantic/component, a DTCG resolver driving the whole thing, a CSS custom property prefix, and your own build command in place of Spektral's Style Dictionary.

# spektral.yml
tokens:
  dir: packages/ui/src/tokens
  cssPrefix: ds
  builtCss:
    - packages/ui/src/styles/tokens.css

You usually will not write it by hand: opening a project whose tokens are not in the default place makes Spektral scan for the real tree, propose a config, and show you the layers, brands and sample tokens it reads back before writing anything. See the spektral.yml reference for every field.

Token Types

$typeExample $valueCSS Output
color"#1e40af"--color-brand-blue-70: #1e40af;
dimension"16px"--spacing-4: 16px;
fontFamily"'Inter', sans-serif"--font-family-body: 'Inter', sans-serif;
fontWeight600--font-weight-semibold: 600;
shadow"0 4px 6px rgba(0,0,0,0.1)"--shadow-md: 0 4px 6px rgba(0,0,0,0.1);
duration"200ms"--duration-fast: 200ms;