Skip to content

Design tokens ​

A token is a named design decision: a color, a spacing, a radius, a font size, a duration. The design system defines the tokens as CSS custom properties, and every component uses them instead of raw values. A brand change, a dark theme or a density change is then a change of tokens, not of hundreds of components.

Three levels ​

LevelNamed afterExampleUsed by
Primitivewhat the value is--color-blue-500, --space-300semantic tokens only
Semanticwhat the value is for--color-action, --color-text-muted, --space-inlinecomponents
Component (optional)a part of one component--button-paddingthat component, to let a parent tune it

Components use semantic tokens: --color-action says why the button is blue, and a theme can change it without touching the button.

Primitives: the palette and the scales.

scss
// Primitive tokens: the raw palette and scales, named after what they are. Components never use them directly.

@layer tokens {
  :root {
    --color-blue-100: oklch(95% 0.03 250deg);
    --color-blue-500: oklch(58% 0.16 250deg);
    --color-blue-700: oklch(45% 0.15 250deg);
    --color-gray-0: oklch(100% 0 0deg);
    --color-gray-100: oklch(96% 0 0deg);
    --color-gray-700: oklch(38% 0 0deg);
    --color-gray-900: oklch(20% 0 0deg);
    --color-red-600: oklch(55% 0.2 27deg);
    --font-family-sans: system-ui, sans-serif;
    --font-size-100: 14px;
    --font-size-200: 1rem;
    --font-size-300: 20px;
    --radius-100: 4px;
    --radius-200: 8px;
    --space-100: 4px;
    --space-200: 8px;
    --space-300: 1rem;
    --space-400: 24px;
  }
}

Semantic tokens, and a dark theme that only changes them:

scss
// Semantic tokens: what a value is for. Components use these; a theme only changes this file.

@layer tokens {
  :root {
    --color-action: var(--color-blue-500);
    --color-action-hover: var(--color-blue-700);
    --color-action-text: var(--color-gray-0);
    --color-danger: var(--color-red-600);
    --color-surface: var(--color-gray-0);
    --color-surface-muted: var(--color-gray-100);
    --color-text: var(--color-gray-900);
    --color-text-muted: var(--color-gray-700);
    --font-size-body: var(--font-size-200);
    --font-size-small: var(--font-size-100);
    --font-size-title: var(--font-size-300);
    --radius-control: var(--radius-100);
    --radius-surface: var(--radius-200);
    --space-inline: var(--space-200);
    --space-stack: var(--space-300);
  }

  // Dark theme: only the semantic tokens change.
  [data-theme='dark'] {
    --color-surface: var(--color-gray-900);
    --color-surface-muted: var(--color-gray-700);
    --color-text: var(--color-gray-0);
    --color-text-muted: var(--color-gray-100);
  }
}

Naming ​

--<category>-<name>, kebab-case: --color-text-muted, --space-inline, --radius-control, --font-size-title, --z-overlay, --duration-fast. Stylelint checks that custom properties are kebab-case (custom-property-pattern, a warning). Sizes in rem are whole steps (1rem, 2rem); the small ones are in px (4px, 8px): a 0.25rem is a warning.

  • Scales use steps with room between them (100, 200, 300…) so that a value can be inserted later.
  • Colors in oklch(): perceptually uniform, so --color-blue-500 and --color-red-500 have the same lightness, and derived shades are predictable.
  • No value in a name (--space-16px): the value may change, the name stays.

Use them ​

scss
.button {
  padding-block: var(--space-inline);
  color: var(--color-action-text);
  background: var(--color-action);
  border-radius: var(--radius-control);
}

A raw color, spacing, radius, shadow, font or z-index belongs in the token files only. Stylelint warns about a named color (red); the review checks the raw values, so that a forgotten #3366cc never reaches main.

Component tokens: an API for parents ​

A component can expose a few custom properties with a default token, so that a parent adjusts it without overriding its internals:

scss
:host {
  --button-padding: var(--space-inline); // the public knob, defaults to a token
}

.button {
  padding: var(--button-padding);
}
scss
.toolbar ds-button {
  --button-padding: var(--space-100); // a denser button, without touching the button's styles
}

Themes ​

A theme is a set of semantic tokens under a selector ([data-theme='dark'], or @media (prefers-color-scheme: dark) to follow the system). Primitives never change: only the mapping from purpose to value does. Switch with one attribute on <html>:

ts
document.documentElement.dataset['theme'] = 'dark';

Animatable tokens ​

A custom property is animated as a string (it jumps). Register it with @property to interpolate it:

css
@property --progress {
  syntax: '<percentage>';
  initial-value: 0%;
  inherits: false;
}

Generated from the JSDoc of the sources.