---
title: Design token — the shape, the architecture, the integrations
layer: schema
status: accepted
owner: Head of Design
scope: Aleris Group
version: v0.1
depends_on: [data-products/tokens/aleris-tokens.css, data-products/tokens/baseline-tokens.json]
propagates_to: []
updated: 2026-08-10
---

# Design token — the shape, the architecture, the integrations

**Status:** accepted · **Owner:** Head of Design · **Scope:** Aleris Group · **Version:** v0.1 · **Updated:** 2026-08-10

**Author:** Torfinn · **Audience:** product teams, designers, and the assistant

**Supersedes:** — · **Related:** [[principles/design-tokens]], [[data-products/tokens/aleris-tokens.css]]

This is the schema — the shape of a token record, how the token layers compose, and how other systems (frameworks, Figma) consume them. It does not restate any token's value: values live in `data-products/tokens/aleris-tokens.css` (source of truth) and `data-products/tokens/baseline-tokens.json` (machine-readable twin), and a document that repeats a value here is a document that can drift from them — which is the failure `constitutional/tokens-are-canonical.md` exists to prevent, and the one this file's predecessor spent four months committing.

The reasoning behind *why* a value is what it is — why petrol/orange/sand, why a perfect-fifth type scale, why cards are 16px — lives in [[principles/design-tokens]], not here. This file is the shape; that file is the argument.

---

## The shape of a token record

Every custom property in `aleris-tokens.css` is documented inline, in the comment on its own declaration line:

```css
--radius-l: 16px;  /* @usage Cards only | @constraint Card tier, decided 2026-08-10 (board card 45) from a live production measurement. Not a general container value */
```

A record has:

- **name** — the `--custom-property` itself.
- **value** — a literal, or a `var()` reference to a lower layer.
- **`@usage`** — what it's for, in plain language.
- **`@constraint`** — what it must not be used for, when that matters.

`data-products/tokens/generate-tokens-json.js` parses this convention directly and produces `baseline-tokens.json`: every record as `{ value, usage, constraint }`, organized by layer and category. **Not every token carries `@usage`/`@constraint` today** — annotation coverage is partial and grows opportunistically, not a completeness requirement. Consult the JSON for lookup; the CSS remains the source of truth if the two ever disagree (regenerate the JSON, per its own header comment).

---

## Three-layer architecture

Aleris tokens use a three-layer model — the same shape IBM Carbon, Google Material, Salesforce Lightning and Atlassian converge on.

**Layer 1 — Primitives.** Raw values: hex codes, pixel values, font names, ratios. The complete Aleris visual palette. Almost never referenced directly in product code.

**Layer 2 — Semantic tokens.** Primitives mapped to usage context. `--text-primary` rather than `--color-petrol-500`. These carry meaning — where a value should be used and why. This is the layer product teams reference in code.

**Layer 3 — Component tokens.** Specific to individual UI components. `--button-primary-bg` rather than `--brand-accent`. These matter when multiple teams build against the same codebase, or when theming.

**Usage:** reference semantic tokens (Layer 2) in code. Use component tokens (Layer 3) when they exist for your component. Never reference primitives directly — typing a hex code is the signal something is wrong.

Each layer references the one below it; no layer requires the one above it to function. A product implementing only primitives still gets a coherent Aleris palette. This is Baseline's expression of Aleris's progressive-enhancement principle, and it has a practical payoff: change one primitive, and every semantic and component token that references it updates automatically. Reference a primitive directly instead, and you've created a manual update point that will drift — the same failure mode `constitutional/tokens-are-canonical.md` generalizes from incidents found the hard way (cards 8, 21, 22, 24).

---

## Usage guidance, by category

Practical starting points — not values, which live in the token file, and not reasoning, which lives in [[principles/design-tokens]].

**Color.** Use `--text-primary` for nearly everything; reach for `--text-secondary` only with a clear hierarchy reason — more than two text colors on a screen is a sign to reconsider the hierarchy, not add a third token. Status colors (`--status-*`, `--goal-*`) communicate state only, never decoration. Goal-status colors always pair with an icon or shape (checkmark, warning triangle, X, dash) — colour alone never carries the meaning, per `constitutional/accessibility-is-foundational.md`.

**Typography.** `--font-weight-regular` (500) for body, `--font-weight-bold` (700) for headings covers almost everything; `--font-weight-light` (300) and `--font-weight-black` (900) are for sparing, deliberate emphasis. Never `font-weight: 400` — Museo Sans doesn't have it, and the browser will synthesize it badly. Most pages use h2 for the page title, h3 for sections, h4 for card headers, body for the rest; h1 (60px) is for communicative surfaces only (landing pages, hero areas) and most instrumental interfaces won't reach for it. Never skip heading levels. Match icon size to the text it accompanies — body gets `--icon-md` (18px), labels get `--icon-sm` (16px).

**Radius.** Cards get `--radius-l`. Other containers get `--radius-s`. Buttons and inputs get `--radius-m`. Badges and tags get `--radius-full`. That covers 90% of cases — see `BASELINE.md` § Component shapes for the corner-radius reasoning behind the card exception.

**Buttons.** One orange (primary) button per screen. A second action is outline or ghost, never a second orange. Two orange buttons visible at once means one of them is wrong.

**Spacing.** Communicative surfaces use the full range (`--spacing-sm` through `--spacing-3xl`); instrumental surfaces use the tighter range (`--spacing-3xs` through `--spacing-lg`). Same tokens, different slice.

**Charts.** Use `--chart-1` through `--chart-N` in sequence, teal first. Beyond 6–8 colors in one chart, the problem is the visualization, not the palette — simplify the data grouping before reaching for chart-13.

---

## Framework bridges

Baseline tokens are plain CSS custom properties. They don't depend on Tailwind, React, or any other framework to function. A product that imports `aleris-tokens.css` into a static HTML page gets the full Aleris design system; a product that imports it into a Next.js app with Tailwind gets the same tokens, plus the option to use utility classes through a bridge layer. This follows the same progressive-enhancement principle as the three-layer architecture above: the base layer is complete on its own, and additional layers make development faster without being load-bearing.

A **bridge** is a thin mapping file that translates Baseline tokens into whatever utility system a project uses. The dependency is one-way — the bridge reads from tokens, tokens never reference the bridge — so a project that drops the bridge loses nothing the tokens didn't already provide directly.

### Tailwind CSS v4

**The bridge is generated. Do not hand-write one.** `data-products/tokens/aleris-tailwind.css` is produced from the token file by `data-products/tokens/generate-tailwind-theme.js` and asserted by `data-products/tokens/tailwind-bridge.test.ts`. Import it after the tokens:

```css
@import "./aleris-tokens.css";
@import "./aleris-tailwind.css";
@import "tailwindcss";
```

That gives `bg-aleris-petrol-500`, `text-aleris-text-secondary`, `p-aleris-md`, `rounded-aleris-m`, `shadow-aleris-e1`, `leading-aleris-body`, and the `aleris-md:` breakpoint variant — 145 of the token file's 363 tokens. Each compiles straight to the token: `bg-aleris-petrol-500` emits `background-color: var(--color-petrol-500)`. The bridge adds nothing to `:root`, so dropping it removes utilities and no values. Which families are **not** bridged, and why each one is out, is listed in the generated file's own header rather than here — a partial mapping that does not say which part is a mapping that reads as complete.

**Two things changed on 2026-08-28 (card 97), both worth knowing if you have the old block in a project.**

**It is generated because the hand-written version drifted.** The example this section used to carry referenced 25 tokens, and **7 of them did not exist**: `--elevation-1/2/3` and `--font-size-h1/h2/h3/h4`, against a token file that declares `--shadow-e0..e3`, `--elevation-card/dropdown/flat/modal`, and `--font-size-xs..2xl`. A consumer copying that block got seven utilities resolving silently to nothing. This is `_packages/README.md` rule 5 in a different costume — a hand-maintained mapping goes stale, and nothing tells you.

**Every name now carries an `aleris-` prefix, and that is a real change in ergonomics.** The old block *replaced* Tailwind's own namespace — `--color-petrol`, `--spacing-4`, `--radius-lg` — so you wrote `bg-petrol` and `p-4`. The generated bridge *adds alongside* it, so you write `bg-aleris-petrol-500` and `p-aleris-md` and Tailwind's defaults keep working. The reason is measured, not stylistic: against Tailwind 4.2.2, a theme variable whose name matches Tailwind's own namespace is emitted into `@layer theme` as a self-reference (`--color-petrol-500: var(--color-petrol-500)`), which is invalid at computed-value time and survives today only because `aleris-tokens.css` is normally imported unlayered. A project that imports the tokens into a cascade layer would get empty values with no error. The prefix removes the collision and the dependency on that accident; it also makes `bg-aleris-*` visibly a token where `bg-[#004851]` is visibly not.

**What this would make wrong:** if the shorter `bg-petrol` form matters more than the safety, the identity map can be restored — but only for names that do not collide with a Tailwind namespace, and the generator should then be the thing that enforces that rather than a reviewer. `--color-*`, `--spacing-*`, `--radius-*`, `--shadow-*`, `--breakpoint-*` and `--ease-*` all collide today.

**The old approximation trade-off is gone with the old approach.** Because the bridge no longer remaps Tailwind's numeric scales onto Baseline's, there is nothing left to approximate: `text-aleris-base` is `--font-size-base` exactly, and Tailwind's own `text-base` is untouched and still means what Tailwind's docs say. The reasoning that governed the old mapping is preserved in [[principles/design-tokens]] §14, which is about a bridge that renames; it no longer describes this file.

### Tailwind CSS v3 (legacy)

```javascript
// Extend, don't replace. Never use default Tailwind colors.
module.exports = {
  theme: {
    extend: {
      colors: {
        petrol: { 100: '#d9e1e2', 300: '#7fa9ae', 500: '#004851' },
        sand: { 100: '#f2ece4', 300: '#e7ceb5', 500: '#d9b48f' },
        orange: { 100: '#fde8df', 300: '#ffbe9f', 500: '#f58c61' },
        gray: { 100: '#d7d2cb', 300: '#9e9281', 500: '#585044' },
        error: '#c14444',
        warning: '#d9b48f',
        goal: {
          achieved: '#2e8540',
          borderline: '#ffb81c',
          missed: '#d4351c',
          'no-data': '#007bc7',
        },
        chart: {
          1: '#0f9081', 2: '#94cbc4', 3: '#577ba3', 4: '#98c9ef',
          5: '#a078c2', 6: '#d8b8ef', 7: '#d77a61', 8: '#f0c3b2',
          9: '#6b9495', 10: '#bed0c0', 11: '#e4ded5', 12: '#d9b48f',
        },
      },
      fontFamily: {
        sans: ['Museo Sans', 'Arial', 'sans-serif'],
      },
      fontSize: {
        // Perfect fifth scale (1.5) from 18px base
        xs: '0.78rem',    // 14px — accessibility floor
        sm: '0.89rem',    // 16px — labels, UI chrome
        base: '1rem',     // 18px — body text (scale anchor)
        md: '1.22rem',    // 22px — h4, card headers (half-step)
        lg: '1.5rem',     // 27px — h3 (×1.5)
        xl: '2.22rem',    // 40px — h2 (×1.5²)
        '2xl': '3.33rem', // 60px — h1 (×1.5³)
      },
      spacing: {
        // Perfect fifth scale (1.5) aligned to 4px grid
        '3xs': '4px', '2xs': '8px', xs: '12px', sm: '16px',
        md: '24px', lg: '36px', xl: '48px', '2xl': '72px', '3xl': '96px',
      },
      borderRadius: {
        DEFAULT: '8px',
        lg: '16px',
        full: '100px',
      },
      boxShadow: {
        soft: '0px 2px 8px rgba(0, 72, 81, 0.08)',
        medium: '0px 4px 16px rgba(0, 72, 81, 0.12)',
        strong: '0px 6px 24px rgba(0, 72, 81, 0.16)',
      },
    },
  },
}
```

### Vanilla CSS

Reference `aleris-tokens.css` directly via import or link. All tokens are CSS custom properties on `:root` and work in any CSS environment — no bridge needed, no framework dependency.

```css
@import url('aleris-tokens.css');

body {
  background-color: var(--surface-page);
  color: var(--text-primary);
  font-family: var(--font-family-primary);
  font-size: var(--type-body-size);
  line-height: var(--type-body-line-height);
}
```

### Adding a new bridge

1. Create a mapping file that reads from `aleris-tokens.css` and translates to the framework's conventions.
2. Keep the dependency one-way: the bridge reads from tokens, tokens never reference the bridge.
3. Document where the mapping is exact and where it approximates.
4. Treat the bridge as project-level infrastructure, not part of the design system itself.

Candidate future bridges: a Figma variables sync, a PDF generation stylesheet, an email-safe CSS subset, a native mobile token export.

---

## Figma sync notes

Changes made to the Figma export values that still need syncing back to Figma, to maintain single-source-of-truth integrity:

| Change | Figma value | Canonical value | Reason |
|---|---|---|---|
| **Full type scale replacement** | title 40px, h1 32px, h2 22px, h3 18px | h1 60px, h2 40px, h3 27px, h4 22px | Rebuilt on perfect fifth (1.5) modular scale. Eliminates h3/body collision. Unified ratio with spacing. |
| **Spacing scale replacement** | 4,8,12,16,24,32,40,48,64,240 | 4,8,12,16,24,36,48,72,96 | Rebuilt on 1.5 ratio aligned to 4px grid. Shared ratio with type scale. |
| Sizes below 14px | label-small 12px, card-comment 13px, button-label-s 13px | Removed entirely | Below 14px accessibility floor. |
| Duplicate token sets | global--tokens---museo--sans---* | Removed | Identical values with auto-generated Figma naming. |
| Icon size tokens | Mixed into text-size styles | Separated to `--icon-s/m/l/xl` | Icons are not typography. |
| Font Awesome tokens | global--tokens---font--awesome-6--pro---* | Removed | Implementation detail, not design token. |
| Shadow values (e1-e3) | black-based rgba | petrol-based rgba | Brand coherence with warm palette. |
| radius-m | Not in Figma | 8px | Needed for input components. |
| radius-l | Not in Figma | 16px | Retired 2026-08-07, reinstated 2026-08-10 (board card 45) at 16px for cards. Needed for card components — not yet in Figma. |
| warning-100 naming | warning-100 | error-500 | The value (#c14444) is an error red, not a warning amber. |
| warning amber | Not in Figma | warning-500 (#d9b48f) | Distinct warning color in sand family. |

---

## File structure

```
data-products/
└── tokens/
    ├── aleris-tokens.css          ← Source of truth (Layer 1)
    ├── aleris-fonts.css           ← Museo Sans @font-face
    ├── baseline-tokens.json       ← Generated machine-readable lookup
    ├── generate-tokens-json.js    ← Regenerates the JSON from the CSS
    └── tokens.test.ts             ← Fitness functions: contrast, retirement guards,
                                      reference-drift, nesting inversions
schemas/
└── design-token.md                ← This file — shape, architecture, integration
principles/
└── design-tokens.md               ← Why each value is what it is
baseline/
├── BASELINE.md                    ← AI instructions (this system's entry point)
├── buttons/, conformance/         ← Rendered fitness surfaces
├── governance/
│   └── aleris-design-governance.md
└── voice/                         ← "Den nära experten" voice guides
```

Moved 2026-08-10 (Phase B step 3, board cards 46/48) from `baseline/tokens/` and `baseline/reference/aleris-design-tokens.md` + `baseline/reference/baseline-token-architecture.md` (the latter two merged and split into this file and `principles/design-tokens.md`). Published routes under `/baseline/...` are unchanged — `lib/baseline-content.ts`'s `resolveBaselineSource` resolves the moved nav items to their new locations, the same mechanism that kept `patterns/` publishing at `/baseline/patterns/*` after its own move (card 32).

---

## What this means for new projects

**Starting a new Aleris product:**

1. Import `data-products/tokens/aleris-tokens.css`. This gives you the full design system as CSS custom properties.
2. If your project uses Tailwind, also import an `aleris-tailwind-bridge.css` built from the Tailwind v4 example above. This gives you utility classes that resolve to Aleris values.
3. Write component styles using `var()` references for semantically important values (colors, typography, surfaces).
4. Use Tailwind utilities for layout mechanics (flex, grid, responsive, spacing) where the bridge is active.
5. Never hard-code values. If a value doesn't exist in the token file, flag it as an open question.

**Starting a project without Tailwind:**

1. Import `data-products/tokens/aleris-tokens.css`. You have everything you need.
2. Write CSS that references tokens via `var()`.
3. Use modern CSS features (nesting, cascade layers, container queries) for layout.
4. The design system works identically — no bridge needed, no framework dependency.

---

## Related

- [[principles/design-tokens]] — why each value is what it is
- [[data-products/tokens/aleris-tokens.css]] — the canonical values
- [[data-products/tokens/baseline-tokens.json]] — the machine-readable twin
- [[constitutional/tokens-are-canonical.md]] — the rule this schema exists in service of: name the token, don't restate its value
- `baseline/BASELINE.md` § Token system — the AI-facing entry point
