React, Typescript, Tailwind CSS

Your Design System Is Not the Components. It's the Tokens.

A component library - buttons, cards, modals, inputs, tables, navigation and a Storybook - sitting on top of three token layers: component tokens, semantic tokens and primitive tokens, with the token layers labelled as the foundation

A lot of teams say they have a design system when what they really have is a component library.

They have:

  • Buttons
  • Cards
  • Modals
  • Inputs
  • Tables
  • Navigation
  • Maybe a Storybook

That is useful. But it is not the foundation of the design system.

The real foundation is the layer underneath all of those components: design tokens.


Tokens are the shared language

Design tokens are the shared language that connects design decisions to implementation.

Instead of saying:

background: #0f172a;
border-radius: 8px;
padding: 16px;

you define intent:

background: var(--color-surface-primary);
border-radius: var(--radius-md);
padding: var(--spacing-4);

That small change becomes extremely important as an application grows.

Components tell you what to render. Tokens tell you how the product behaves visually.

A component library might define a Button. But the button should not decide that its background colour is blue. It should consume something like:

Button

component token

semantic token

primitive token

For example:

button.primary.background

color.action.primary

blue.700

A button resolving its background through three token layers - button.primary.background, then color.action.primary, then blue.700 - with a rebrand shown as a single change at the semantic layer that swaps blue.700 for purple.700 while the button, badge, tab and navigation components stay untouched

This separation matters.

If tomorrow the brand changes from blue to purple, you should not have to modify every button, badge, tab and navigation component. You change the token. The system changes with it.


A practical token architecture

A scalable design system usually benefits from three layers.

Three token layers stacked from the bottom up - primitive tokens as raw values like blue.500 and spacing.4, semantic tokens as meaning like color.text.primary and color.action.primary, and component tokens like button.primary.background - each annotated with who is allowed to consume it

1. Primitive tokens

These are raw values.

{
  "blue": {
    "50": "#eff6ff",
    "500": "#3b82f6",
    "900": "#1e3a8a"
  },
  "spacing": {
    "2": "0.5rem",
    "4": "1rem",
    "6": "1.5rem"
  }
}

Primitive tokens generally should not be used directly by application code. They are ingredients.

2. Semantic tokens

Semantic tokens describe meaning.

{
  "color": {
    "background": {
      "default": "{white}",
      "subtle": "{slate.50}"
    },
    "text": {
      "primary": "{slate.950}",
      "secondary": "{slate.600}"
    },
    "action": {
      "primary": "{blue.700}"
    }
  }
}

Now your application is no longer coupled to a specific colour. It is coupled to intent.

That is a much more stable abstraction.

3. Component tokens

For larger systems, components can add another layer.

{
  "button": {
    "primary": {
      "background": "{color.action.primary}",
      "foreground": "{color.text.inverse}",
      "radius": "{radius.md}"
    }
  }
}

This allows component behaviour to evolve without changing the underlying global semantics.

Each layer answers a different question

Primitives answer what values exist. Semantic tokens answer what this value means in the product. Component tokens answer how this particular component uses that meaning. Application code should only ever be talking to the top two - the moment it reaches down to a primitive, the abstraction has leaked.


Why this becomes critical in white-labelled products

This is where design tokens stop being a design-team convenience and become an architecture decision.

Imagine a platform serving multiple organisations. Each organisation may want:

  • Different colours
  • Different logos
  • Different fonts
  • Different border radii
  • Different navigation styling
  • Light or dark modes
  • Product-specific themes

Without tokens, white-labelling quickly becomes:

if tenant === schoolA ...
if tenant === schoolB ...
if tenant === customerC ...

That approach does not scale.

On the left, tenant branching - if tenant is school A, if tenant is school B, if tenant is customer C - multiplying inside every component. On the right, one shared UI consuming semantic tokens, with a tenant theme and brand configuration underneath, so three tenants flow through the same components

With tokens, the component tree stays the same.

Shared UI

Semantic tokens

Tenant theme

Brand configuration

The organisation changes the configuration. The application does not change.


Design tokens also create boundaries

One of the biggest benefits is preventing application developers from accidentally creating a second design system inside the product.

Instead of:

<div className="bg-[#15254c] rounded-[11px] px-[17px]">

you want something closer to:

<div className="bg-surface rounded-md px-4">

or:

<Button variant="primary" />

Developers choose intent, not arbitrary visual values.

That gives the design system something incredibly valuable: control over entropy.

Arbitrary values are a second design system

Every bg-[#15254c] in application code is a design decision that the design system does not know about. One is harmless. Two hundred of them, across four applications, is a shadow design system with no owner, no theme support and no way to rebrand. The token layer is only a boundary if the escape hatch is treated as a code-review defect.


Tailwind and shadcn make this even more useful

A stack such as:

React
+
Tailwind CSS
+
shadcn/ui
+
CSS variables
+
Design tokens

works extremely well for this model.

shadcn provides the component foundations. Tailwind provides the utility layer. CSS variables become the runtime token interface.

Your theme might expose:

:root {
  --background: 0 0% 100%;
  --foreground: 222 47% 11%;

  --primary: 221 83% 53%;
  --primary-foreground: 210 40% 98%;

  --radius: 0.75rem;
}

A tenant can then override those values:

[data-tenant="acme-school"] {
  --primary: 265 80% 50%;
  --radius: 0.5rem;
}

The components remain untouched.

That is the important part.


Tokens should go beyond colour

Colour usually gets the attention, but a mature token system can represent much more:

A grid of twelve token categories - colour, typography, spacing, radius, shadows, borders, motion, breakpoints, z-index, opacity, icon sizing and layout density - with a second row of behavioural tokens: density.compact and density.comfortable, motion.fast and motion.normal, navigation.width and content.maxWidth

Colour
Typography
Spacing
Radius
Shadows
Borders
Motion
Breakpoints
Z-index
Opacity
Icon sizing
Layout density

Eventually you can even represent product behaviour such as:

density.compact
density.comfortable

motion.fast
motion.normal

navigation.width

content.maxWidth

The result is a UI system that becomes configurable without becoming inconsistent.


The interesting architectural shift

The traditional UI architecture looks something like:

Designer

Figma

Developer

CSS

A token-driven architecture looks more like:

Design decisions

Design tokens

Theme configuration

Component library

Applications

Two pipelines. The traditional one runs designer to Figma to developer to CSS, with a hand-off gap in the middle. The token-driven one runs design decisions to design tokens to theme configuration to component library to applications, with the token layer marked as the contract shared by design and engineering

Now the token layer becomes a contract shared between design and engineering.

That is much more powerful than simply maintaining matching colour palettes in Figma and CSS.


Treat the design system like a platform

Once multiple applications consume the same UI kit, the design system should be treated almost like an internal platform.

For example:

packages/
  design-tokens/
  ui/
  icons/
  typography/
  themes/

apps/
  admin/
  student/
  parent/
  teacher/

Applications should consume the system. They should not redefine it. If you want a vocabulary for what the system contains, Atomic Design supplies one - atoms, molecules, organisms - and tokens are what sit beneath the atoms.

The moment every application starts introducing its own colours, spacing rules and component variants, the design system starts fragmenting. This is the same line drawn in micro-frontends without duplicating your React code: the design system belongs in the shared code boundary, and tokens are the part of it that must be shared most strictly of all.


The rule I would use

A simple rule for engineering teams:

If a visual decision is likely to appear more than once, it probably deserves a token.

And another:

Application code should express intent. The design system should decide appearance.

That distinction makes design systems significantly easier to scale. Especially when you're building multiple products, multiple themes or a white-labelled SaaS platform.

The button component is useful.

The token that controls every button across every application is the actual leverage.

That is where the design system starts becoming architecture.

Previous
TypeScript 7 - the compiler went native