EduFlow

Section 2 · Design Tokens

Three Tiers: Primitive, Semantic, Component

20 min3 min readCode playground

Lesson at a glance

Structure design tokens in primitive, semantic and component tiers as CSS custom properties, so one change of intent updates every component that shares it.

  1. 1Tier 1: primitives
  2. 2Tier 2: semantic tokens
  3. 3Tier 3: component tokens
  4. 4How the browser resolves a token
  5. 5Not just colors
  6. 6A quick way to check your tiers

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

Northwind’s first token file was a list of colors: --blue-600, --gray-200, --red-500. Every component used them directly. It looked tidy until the Tidewater acquisition, when someone asked a simple question: “Which blues are Northwind brand blues, and which are just links?” Nobody could answer without reading 300 files. A flat list of colors records values. It doesn’t record decisions.

Tier 1: primitives

Primitives are the palette: every raw value the system allows, named by what it is.

CSS
:root {
  --nw-blue-600: #1f5ae0;
  --nw-blue-700: #1848b8;
  --nw-gray-900: #1f2933;
  --nw-gray-200: #d9dee3;
  --nw-white: #ffffff;
  --nw-space-2: 0.5rem;
  --nw-space-4: 1rem;
}

Primitives answer “which values exist?” They deliberately carry no meaning. --nw-blue-600 doesn’t know whether it’s a button, a link or a chart line.

Tier 2: semantic tokens

Semantic tokens name an intent and point at a primitive:

CSS
:root {
  --nw-color-action-bg: var(--nw-blue-600);
  --nw-color-action-bg-hover: var(--nw-blue-700);
  --nw-color-action-text: var(--nw-white);
  --nw-color-text: var(--nw-gray-900);
  --nw-color-border: var(--nw-gray-200);
}

This is the tier people use most, and the one themes change. When dark mode or the Tidewater brand arrives, you remap these names to different primitives and leave every component alone.

Tier 3: component tokens

Component tokens name one component’s adjustable parts and point at semantic tokens:

CSS
.nw-button {
  --nw-button-bg: var(--nw-color-action-bg);
  --nw-button-text: var(--nw-color-action-text);
  --nw-button-padding-x: var(--nw-space-4);

  background: var(--nw-button-bg);
  color: var(--nw-button-text);
  padding: var(--nw-space-2) var(--nw-button-padding-x);
}

Now three kinds of change each have one obvious place:

Component token button-bg points to semantic token color-action-bg, which points to primitive blue-600 with the value 1f5ae0. Each tier is changed for a different reason. --nw-button-bg --nw-color-action-bg --nw-blue-600 Component Semantic Primitive Change one component Change a theme or brand Change the palette #1f5ae0
Each tier references only the tier below it
  • New palette (a slightly more accessible blue): change the primitive.
  • New theme or brand (dark mode, Tidewater): remap semantic tokens.
  • One component needs to differ (the marketing hero button uses a larger padding): override that component’s token.

The rule that makes this work: components reference semantic tokens, never primitives. The moment a component uses --nw-blue-600 directly, it stops following themes, and you have rebuilt the flat list with longer names.

How the browser resolves a token

Custom properties inherit like color does. When the browser computes styles for a button, it looks up --nw-button-bg on the button itself, finds var(--nw-color-action-bg), then looks that up on the button too, where the value is inherited from the nearest ancestor that set it. That’s why you can re-theme part of a page by setting a semantic token on a container:

CSS
.campaign-banner {
  --nw-color-action-bg: var(--nw-green-600);
}

Every button inside the banner turns green; every button outside stays blue. No new classes, no component changes. This scoping behavior is the foundation for themes in the next lessons.

Not just colors

Every category of token gets the same treatment. Spacing primitives are a scale (--nw-space-1 to --nw-space-12); semantic spacing names a role, such as --nw-space-inset-md for padding inside a card or --nw-space-stack-sm for the gap between stacked form fields. Typography primitives are font families, sizes and weights; semantic typography names styles such as --nw-font-body or --nw-font-heading-lg. Radii, shadows and motion durations follow the same pattern.

For scale, Northwind today has about 180 primitives, 120 semantic tokens and 90 component tokens. The semantic tier is where most of the design thinking happens, and it’s the tier designers and engineers should reach for first. If a team keeps asking for a primitive, that usually means a semantic token is missing, and the right fix is to add the role, not to hand out the raw value.

A quick way to check your tiers

Open any component’s CSS and search for your primitive prefix, such as --nw-blue or --nw-gray. In a healthy system those names appear in exactly one place: the semantic token definitions. Northwind runs this as a lint rule in CI, which you will meet in Section 4. In the exercise, you add the semantic and component tiers to a button that currently skips straight to a primitive. Next, you name these tokens so they still make sense in three years.

Key takeaways

  • Primitive tokens name raw values (--nw-blue-600); semantic tokens name intent (--nw-color-action-bg); component tokens name one component’s knobs.
  • Components reference semantic tokens, never primitives, so re-theming only changes the semantic tier.
  • Declare component tokens on the component’s selector, not on :root, so they resolve against overrides in that part of the page.
  • Add component tokens sparingly; each one is public API you will have to support.

Further reading

Check your understanding

3 questions

Question 1 of 3

Marketing wants every primary action on the site to turn green for a campaign. With a three-tier system, which tier do you change?

Question 2 of 3

Why does the button inside .promo stay blue?

CSS
:root {
  --nw-color-action-bg: var(--nw-blue-600);
  --nw-button-bg: var(--nw-color-action-bg);
}
.promo { --nw-color-action-bg: var(--nw-green-600); }
.nw-button { background: var(--nw-button-bg); }
Question 3 of 3

A teammate proposes a component token for every CSS property of every component. What’s the main cost?

Practice

Code playground

Give the button three tiers

The primitives are done, but .nw-button reaches straight for --nw-blue-600, so no theme can ever change it. Add the two missing tiers:

  1. On :root, declare the semantic tokens --nw-color-action-bg and --nw-color-action-text, pointing at the right primitives.
  2. On .nw-button, declare the component tokens --nw-button-bg and --nw-button-text, pointing at the semantic tokens, and use them for background and color.

The button must look exactly as it does now, and no .nw-button rule may mention a --nw-blue-… or --nw-white primitive.

<section class="app">
  <button class="nw-button" id="main">Assign driver</button>
</section>
<section class="promo">
  <button class="nw-button" id="promo">Start free trial</button>
</section>Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

Preview

Console

(no output)

Checks

  • The button still renders blue with white text
  • :root defines --nw-color-action-bg and --nw-color-action-text
  • The button’s background comes from --nw-button-bg
  • Overriding --nw-color-action-bg on .promo re-colors only the promo button
  • No .nw-button rule references a primitive token