CSS :root() & Custom Properties

Design Tokens for Colour, Type, Spacing & Themes

Course progress

Lesson 1 of 9 – Overview & mental model Lesson 2 of 9 – :root & var() basics Lesson 3 of 9 – Colour tokens & themes Lesson 4 of 9 – Typography tokens Lesson 5 of 9 – Spacing & sizing tokens Lesson 6 of 9 – Radius, borders & shadows Lesson 7 of 9 – Component-level tokens Lesson 8 of 9 – Responsive & state tokens Lesson 9 of 9 – Final example & checklist

Step 1

Welcome & the design token mental model

This page is both a tutorial and a working example of how to use :root and CSS custom properties as design tokens for colour, type, spacing, radius, and more.

What you’ll learn

By the end, you’ll be able to:

  • Use :root and var() to define global design tokens.
  • Create light/dark colour palettes with a single theme switch.
  • Build a consistent typography scale with font families and sizes.
  • Set up spacing and sizing tokens you can reuse everywhere.
  • Standardise border radius, borders, and shadows.
  • Layer “component-level” tokens on top of your global system.

Mini example: one token reused everywhere

Here’s the core idea: define a value once, reuse it everywhere:

/* Global token */
:root {
  --brand-forest: #3c5d4a;
}

/* Components that consume the token */
.button-primary {
  background-color: var(--brand-forest);
  color: #fff;
}

.badge-accent {
  border: 1px solid var(--brand-forest);
  color: var(--brand-forest);
}

Change --brand-forest in one place and every button, badge, or card that uses it will update automatically.

Key terms we’ll use

  • Custom property – the underlying CSS feature (e.g. --color-bg).
  • Design token – a named value used across a design system.
  • Global token – a token defined in :root for site-wide use.
  • Component token – a token scoped to a component (e.g. .button).
Lesson 1 of 9
Step 2

Setting up :root & var() basics

We’ll start by defining global tokens in :root and reading them with var(), including safe fallbacks.

Why :root for tokens?

:root selects the root element of the document (the <html> element). It’s ideal for tokens because:

  • It’s only defined once per document.
  • Values cascade to the entire page.
  • You can override tokens later in more specific scopes.
:root {
  /* Colour tokens */
  --color-bg: #f8f3f0;
  --color-surface: #ffffff;
  --color-text: #1e2f26;
  --color-accent: #3c5d4a;

  /* Typography tokens */
  --font-family-base: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-family-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace;
}

Using var() with fallbacks

var() reads a custom property. You can optionally provide a fallback:

.page {
  background-color: var(--color-bg, #ffffff);
  color: var(--color-text, #222222);
}

pre {
  font-family: var(--font-family-mono, monospace);
}

The fallback is used when the token isn’t defined, which is handy when refactoring older CSS in stages.

Quick check: where do tokens live?

For global design tokens that should apply site-wide, which selector is usually the best home?

Lesson 2 of 9
Step 3

Colour tokens & light/dark themes

Colour tokens let you define palettes that support light and dark modes without rewriting every selector.

Core colour tokens

Start with a small set of roles instead of hundreds of direct hex codes:

  • --color-bg – page background
  • --color-surface – card and panel background
  • --color-text – body text
  • --color-muted – secondary text
  • --color-accent – brand accent colour
  • --color-accent-soft – subtle accent background
:root {
  color-scheme: light dark;

  --color-bg: #f8f3f0;
  --color-surface: #ffffff;
  --color-text: #1e2f26;
  --color-muted: #4c6258;

  --color-accent: #3c5d4a;
  --color-accent-soft: #e0ece5;
}

Hooking into your dark mode toggle

If your site toggles a .dark-mode class on <body> (as this template does), you can flip the tokens there:

body.dark-mode {
  --color-bg: #070b09;
  --color-surface: #111a15;
  --color-text: #f5f5f5;
  --color-muted: #c6d1cc;

  --color-accent: #90c3a2;
  --color-accent-soft: #1b2a22;
}

All components that reference these tokens automatically adapt when dark mode is on.

Example: token-driven card

Card styles powered by tokens

Notice how the card never references raw hex colours, only tokens.

.token-card {
  background-color: var(--color-surface);
  color: var(--color-text);
  border-radius: var(--radius-md);
  border: 1px solid var(--color-accent-soft);
  box-shadow: var(--shadow-soft);
}

.token-card__eyebrow {
  color: var(--color-muted);
}

.token-card__cta {
  background-color: var(--color-accent);
  color: var(--color-on-accent, #ffffff);
}
Lesson 3 of 9
Step 4

Typography tokens

Typography tokens capture font families, font sizes, line heights, and font weights so that headings and body copy stay consistent as your site grows.

Font family & weight tokens

First, define your stacks and common weights:

:root {
  --font-family-base: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-family-heading: "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
  --font-family-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace;

  --font-weight-regular: 400;
  --font-weight-medium: 500;
  --font-weight-bold: 700;
}

Type scale tokens

Next, define a simple scale. The exact values are less important than using the same ones everywhere:

:root {
  --font-size-100: 0.875rem; /* small text */
  --font-size-200: 1rem;     /* body */
  --font-size-300: 1.125rem; /* lead text */
  --font-size-400: 1.375rem; /* h3 */
  --font-size-500: 1.75rem;  /* h2 */
  --font-size-600: 2.25rem;  /* h1 */

  --line-height-tight: 1.2;
  --line-height-normal: 1.5;
  --line-height-relaxed: 1.7;
}

You can later combine these with clamp() for fluid type, but a fixed scale is a solid start.

Example: applying the scale

Heading and body text with tokens

Notice how each element references the same small set of tokens.

h1 {
  font-family: var(--font-family-heading);
  font-size: var(--font-size-600);
  line-height: var(--line-height-tight);
  font-weight: var(--font-weight-bold);
}

h2 {
  font-family: var(--font-family-heading);
  font-size: var(--font-size-500);
  line-height: var(--line-height-tight);
}

body {
  font-family: var(--font-family-base);
  font-size: var(--font-size-200);
  line-height: var(--line-height-normal);
}

Quick check: typography tokens

Which of these is the best name for a reusable body text size token?

Lesson 4 of 9
Step 5

Spacing & sizing tokens

Spacing tokens keep margins, paddings, and gaps consistent. They’re also a great place to start when migrating legacy layouts.

Defining a spacing scale

A simple 0–6 scale is often enough:

:root {
  --space-0: 0;
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
}

You can layer larger tokens on top later (e.g. --space-page).

Using spacing tokens in layout

Stack and cluster patterns with tokens

Spacing tokens work beautifully with layout utility classes.

.stack > * + * {
  margin-block-start: var(--space-3);
}

.cluster {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.card {
  padding: var(--space-4);
  border-radius: var(--radius-md);
}

Size tokens

You can also define common fixed sizes:

:root {
  --size-input-height: 2.5rem;
  --size-container-max: 70rem;
  --size-avatar-sm: 2rem;
  --size-avatar-md: 3rem;
}
Lesson 5 of 9
Step 6

Radius, borders & shadows

Tokens for corners and shadows make it easy to keep cards, buttons, and chips visually related without guessing border-radius values each time.

Radius tokens

Define a small radius scale:

:root {
  --radius-none: 0;
  --radius-sm: 0.15rem;
  --radius-md: 0.35rem;
  --radius-lg: 0.75rem;
  --radius-pill: 999px;
}

Border & shadow tokens

Pair radius tokens with borders and shadows:

:root {
  --border-width-hairline: 1px;
  --border-width-strong: 2px;
  --border-color-default: var(--color-accent-soft);

  --shadow-none: none;
  --shadow-soft: 0 2px 6px rgba(0, 0, 0, 0.08);
  --shadow-strong: 0 4px 14px rgba(0, 0, 0, 0.16);
}

Example: tokenised button

Button with radius & shadow tokens

All visual decisions are stored in tokens, so swapping style later is trivial.

.button {
  --button-radius: var(--radius-pill);
  --button-shadow: var(--shadow-soft);

  border-radius: var(--button-radius);
  box-shadow: var(--button-shadow);
  border: var(--border-width-hairline) solid transparent;
}

.button:focus-visible {
  outline: none;
  box-shadow:
    0 0 0 2px var(--color-bg),
    0 0 0 4px var(--color-accent);
}
Lesson 6 of 9
Step 7

Component-level tokens

Component tokens sit on top of global tokens, making it easy to tweak one component without breaking the rest of the system.

Layering component tokens

Use global tokens inside component tokens, then reference only the component-level names in your rules:

.button {
  /* Component tokens */
  --button-bg: var(--color-accent);
  --button-bg-hover: color-mix(in srgb, var(--color-accent) 90%, #000 10%);
  --button-text: var(--color-on-accent, #ffffff);
  --button-padding-inline: var(--space-4);
  --button-padding-block: var(--space-2);

  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding-inline: var(--button-padding-inline);
  padding-block: var(--button-padding-block);
  border-radius: var(--radius-pill);
  background-color: var(--button-bg);
  color: var(--button-text);
}

Variants by overriding tokens

Primary vs secondary buttons via overrides

Each variant overrides only the component tokens it needs.

.button--secondary {
  --button-bg: transparent;
  --button-bg-hover: var(--color-accent-soft);
  --button-text: var(--color-accent);
  --button-border-color: var(--color-accent);

  border: var(--border-width-hairline) solid var(--button-border-color);
  background-color: var(--button-bg);
}

Contextual overrides

You can also override tokens at a higher level, like a hero section:

.hero {
  --color-accent: #dbb5b4;  /* dusty rose in this section */
}

.hero .button {
  /* Buttons inside .hero use the new accent automatically */
}
Lesson 7 of 9
Step 8

Responsive & state tokens

Finally, collect the “extras”: breakpoints, transition timings, z-index layers, and state colours, all as tokens.

Timing & motion tokens

Group your motion decisions together:

:root {
  --duration-fast: 120ms;
  --duration-normal: 200ms;
  --duration-slow: 320ms;

  --easing-standard: cubic-bezier(0.2, 0, 0.13, 1);
}

.button {
  transition:
    background-color var(--duration-normal) var(--easing-standard),
    box-shadow var(--duration-fast) var(--easing-standard);
}

Layer & breakpoint tokens

Z-index and width tokens help avoid magic numbers:

:root {
  --z-base: 0;
  --z-dropdown: 1000;
  --z-modal: 1100;

  --layout-max-width: 72rem;
}

Media query conditions can’t (yet) use var() everywhere, but you can still reuse breakpoint widths inside your rules:

@media (min-width: 48rem) {
  .layout {
    max-width: var(--layout-max-width);
    margin-inline: auto;
  }
}

Quick check: good token candidates

Which of these is least helpful as a token?

Lesson 8 of 9
Step 9

Putting it all together

Let’s combine colour, type, spacing, radius, and component tokens into a single, tokenised layout example.

Tokenised profile card example

A small layout driven entirely by tokens

The HTML doesn’t know anything about colours, fonts, or spacing. The CSS tokens carry all of that knowledge.

<article class="token-card">
  <p class="token-card__eyebrow">CSS design tokens</p>
  <h2 class="token-card__title">Build once, theme everywhere</h2>
  <p>Use :root and custom properties to define colours,
    spacing, type, and radius just once.</p>
  <button class="button">View tokens</button>
</article>

.token-card {
  padding: var(--space-4);
  border-radius: var(--radius-md);
  background-color: var(--color-surface);
  color: var(--color-text);
  box-shadow: var(--shadow-soft);
  max-width: 26rem;
}

.token-card__eyebrow {
  font-size: var(--font-size-100);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-muted);
  margin-block-end: var(--space-2);
}

.token-card__title {
  font-size: var(--font-size-400);
  margin-block-end: var(--space-3);
}

.token-card p {
  margin-block-end: var(--space-4);
}

Checklist: robust token system

  • Define tokens in :root for: colour, type, spacing, radius, shadows, motion, and layers.
  • Keep names neutral and reusable (e.g. --space-3, not --card-gap).
  • Use tokens everywhere instead of raw hex, pixel, or timing values.
  • Use component-level tokens to create variants without duplicating CSS.
  • Hook your light/dark theme toggle into token overrides, not per-component rules.
  • Introduce tokens gradually into legacy code using var() fallbacks.
Lesson 9 of 9