---
name: token-architecture
description: Design and govern a three-tier CSS custom property token system (primitive → semantic → component), including semantic alias layers, light/dark theming, and DTCG-compatible source generation.
---

# Skill: token-architecture

Load this skill when the primary task is designing or auditing a token
**system** — the architecture that governs how tokens are named, derived, and
organized. Do not load this skill for routine surface work where seeding a
token block is sufficient; the seed token block in `frontend-engineering` step 2
covers that case. Load `token-architecture` when:

- The product needs a token system designed from scratch
- An existing token system has hardcoded value drift and needs an audit
- A new semantic alias layer is being added (e.g., adding a dark-theme override)
- Multi-platform distribution requires DTCG-compatible source generation

---

## Three-tier architecture

The one-way dependency rule governs the entire system:

```
Primitive  →  Semantic  →  Component
```

- **Primitives** are raw values: `#5e6ad2`, `16px`, `1.5`. They have no meaning
  beyond their value. They are defined once at the top of the CSS file (or in
  the design tool) and referenced only by the semantic layer. Component tokens
  must not reference primitives directly — only semantics.
- **Semantics** are role assignments: `--ds-color-primary` points to a primitive.
  Semantics carry the design intent. When the brand changes, only the semantic
  layer's pointer changes; component tokens remain stable.
- **Components** are usage tokens scoped to a specific component:
  `--btn-bg: var(--ds-color-primary)`. They reference semantics, never
  primitives.

**Violation to refuse:** a component token that reads
`var(--primitive-color-indigo-500)` directly. This is a one-way dependency
violation — if the primitive moves, every component that references it must
be updated rather than only the semantic pointer.

---

## Naming conventions

The `--ds-` namespace prefix is required for all system tokens. It signals
"design system" and prevents collisions with author-defined custom properties
or third-party libraries.

### Semantic naming rules

| Wrong | Right | Why |
|---|---|---|
| `--color-blue-500` | `--ds-color-primary` | Implementation detail leaks into the name; survives no redesign |
| `--spacing-16px` | `--ds-space-4` | Value leaks into the name; breaks if the scale changes |
| `--radius-8` | `--ds-radius-md` | Same problem; semantic size name is stable |
| `--font-inter` | `--ds-font-body` | Font family leaks; semantic role is stable |

The naming rule: a token name encodes **role**, never **implementation**.
`--ds-color-primary` survives a rebrand to a different hue. `--color-blue-500`
does not.

---

## Scale derivation

Derive spacing, typography, and radius scales from a single organizing ratio.
Three ratios cover most design needs:

| Ratio | Value | Best for |
|---|---|---|
| Minor third | 1.25 | Compact, data-dense surfaces (dashboards, admin) |
| Major third | 1.333 | General product surfaces |
| Golden ratio | 1.618 | Editorial, marketing, high-visual-weight surfaces |

### Spacing scale (minor third, 4px base)

```
--ds-space-1:  4px          (base)
--ds-space-2:  8px          (base × 1.25 → round to 8)
--ds-space-3:  12px
--ds-space-4:  16px
--ds-space-5:  24px         (major jump — section separation)
--ds-space-6:  32px
--ds-space-7:  48px
--ds-space-8:  64px
```

### Typography scale (major third)

```
--ds-text-sm:   0.75rem     (12px)
--ds-text-base: 0.875rem    (14px)
--ds-text-lg:   1rem        (16px)
--ds-text-xl:   1.125rem    (18px)
--ds-text-2xl:  1.25rem     (20px)
--ds-text-3xl:  1.5rem      (24px)
```

Derive line-height from the scale ratio — not from a fixed pixel value. A
unitless multiplier (1.25, 1.5, 1.75) scales with the font size and avoids
WCAG 1.4.12 (Text Spacing) violations.

### Radius scale

```
--ds-radius-sm:   4px       (chips, badges, small controls)
--ds-radius-md:   8px       (buttons, cards)
--ds-radius-lg:   12px      (modals, sheets, large containers)
--ds-radius-full: 9999px    (pills)
```

---

## Semantic alias layer

The semantic layer maps roles to primitives. A complete minimum viable set:

| Role | Light theme default | Dark theme override | Meaning |
|---|---|---|---|
| `--ds-color-surface` | `#ffffff` | `#0d0d0d` | Primary background |
| `--ds-color-surface-alt` | `#f8fafc` | `#1a1a1a` | Secondary/alternate background |
| `--ds-color-on-surface` | `#1a202c` | `#e2e8f0` | Primary text on surface |
| `--ds-color-on-surface-2` | `rgba(0,0,0,0.60)` | `rgba(255,255,255,0.60)` | Secondary/muted text |
| `--ds-color-primary` | `#5e6ad2` | `#8b93e8` | Brand / interactive primary |
| `--ds-color-on-primary` | `#ffffff` | `#ffffff` | Text on primary |
| `--ds-color-error` | `#dc2626` | `#f87171` | Error state |
| `--ds-color-on-error` | `#ffffff` | `#ffffff` | Text on error |
| `--ds-color-warning` | `#d97706` | `#fbbf24` | Warning state |
| `--ds-color-success` | `#16a34a` | `#4ade80` | Success state |
| `--ds-color-info` | `#0284c7` | `#38bdf8` | Informational state |
| `--ds-color-disabled` | `rgba(0,0,0,0.38)` | `rgba(255,255,255,0.38)` | Disabled content |
| `--ds-color-overlay` | `rgba(0,0,0,0.50)` | `rgba(0,0,0,0.70)` | Modal backdrop |
| `--ds-color-outline` | `rgba(0,0,0,0.12)` | `rgba(255,255,255,0.12)` | Borders, dividers |

### Light/dark theme switching

Declare the semantic layer inside `:root` for light mode. Override inside
`@media (prefers-color-scheme: dark)` for automatic system-preference
switching, and/or inside `[data-theme="dark"]` for an explicit user toggle:

```css
:root {
  --ds-color-surface: #ffffff;
  --ds-color-primary: #5e6ad2;
  /* ... all semantic tokens ... */
}

@media (prefers-color-scheme: dark) {
  :root {
    --ds-color-surface: #0d0d0d;
    --ds-color-primary: #8b93e8;
    /* ... dark overrides only ... */
  }
}

[data-theme="dark"] {
  --ds-color-surface: #0d0d0d;
  --ds-color-primary: #8b93e8;
  /* identical to the media query block — both must be kept in sync */
}
```

The `[data-theme]` attribute toggle and the `prefers-color-scheme` query are
independent — a user who has set `data-theme="dark"` expects the dark theme
regardless of system preference. Both must be maintained.

---

## Component token pattern

Component tokens scope semantic tokens to a component's internal anatomy.
They are the only layer allowed to reference semantics (never primitives).

**Button token example — three-tier chain:**

```css
/* Tier 1: Primitive (defined once, never referenced by components) */
:root {
  --primitive-color-indigo-500: #5e6ad2;
  --primitive-color-indigo-700: #4338ca;
  --primitive-color-white: #ffffff;
}

/* Tier 2: Semantic (roles, not values) */
:root {
  --ds-color-primary: var(--primitive-color-indigo-500);
  --ds-color-primary-hover: var(--primitive-color-indigo-700);
  --ds-color-on-primary: var(--primitive-color-white);
}

/* Tier 3: Component (anatomy, references semantics only) */
.btn {
  --btn-bg:           var(--ds-color-primary);
  --btn-bg-hover:     var(--ds-color-primary-hover);
  --btn-text:         var(--ds-color-on-primary);
  --btn-radius:       var(--ds-radius-md);
  --btn-padding-x:    var(--ds-space-4);
  --btn-padding-y:    var(--ds-space-2);

  background-color: var(--btn-bg);
  color:            var(--btn-text);
  border-radius:    var(--btn-radius);
  padding:          var(--btn-padding-y) var(--btn-padding-x);
}

.btn:hover,
.btn:focus-visible {
  background-color: var(--btn-bg-hover);
}
```

The chain: `.btn` reads `--btn-bg`; `--btn-bg` reads `--ds-color-primary`;
`--ds-color-primary` reads `--primitive-color-indigo-500`. When the brand
changes `--primitive-color-indigo-500` to a new hue, `.btn` picks it up
automatically through the chain.

---

## DTCG export

The Design Tokens Community Group (DTCG) format is the correct structure for
multi-platform distribution (producing iOS/Android values from the same source,
or supplying a design tool with a token JSON it understands).

DTCG token format — each token uses `$type`, `$value`, and optionally
`$description`:

```json
{
  "color": {
    "primary": {
      "$type": "color",
      "$value": "#5e6ad2",
      "$description": "Brand primary — interactive elements and focus indicators"
    },
    "surface": {
      "$type": "color",
      "$value": "#ffffff",
      "$description": "Primary surface background"
    }
  },
  "spacing": {
    "4": {
      "$type": "dimension",
      "$value": "16px",
      "$description": "Base unit × 4 — standard internal padding"
    }
  }
}
```

**When to produce DTCG output:** when the token system feeds a build pipeline
that generates platform-specific token files (CSS custom properties, Swift
UIColor, Android XML, Figma variables). For a web-only project that consumes
tokens directly as CSS custom properties, DTCG export is optional.

---

## Token audit

To detect token drift — hardcoded values in CSS that should be tokens — run:

```bash
grep -E "#[0-9a-fA-F]{3,6}|rgba?\(|hsl\(|[0-9]+px" <file.css>
```

The output should return **only** the `:root` / primitive-definition block.
Any hardcoded colour or spacing value outside that block is a violation.

**Triage priority:**

1. Hardcoded colour values in component or semantic files — these break
   theming and must be migrated to tokens.
2. Magic pixel values for spacing (`margin: 13px`) — migrate to the scale.
3. Hardcoded font sizes — migrate to the type scale.
4. Hardcoded `z-index` numbers — migrate to a named z-index scale.

---

## Governance checklist

A token architecture encodes organizational decisions. Before finalizing:

- [ ] Who owns the **primitive layer**? Changes here affect every component
  through the chain. Primitives should change only with explicit stakeholder
  sign-off.
- [ ] Who owns the **semantic layer**? A new semantic role (e.g. a new status
  color) should require review from both design and engineering.
- [ ] Are **component tokens** scoped to the component that owns them? Shared
  component tokens across unrelated components are a coupling smell.
- [ ] Is the **DTCG export** needed? If multi-platform, a build step that
  produces platform tokens from the DTCG source is part of the governance
  contract.
- [ ] Is the **dark-theme override** kept in sync with the light-theme semantic
  layer? Every new semantic token added to light mode must have a corresponding
  dark override (or an explicit decision that it inherits the light value).

## Output rendering

Key–value / one record — For a single record's fields, use an aligned key: value list, not a two-row table.
