CSS token variables for spacing, typography, and breakpoints arranged on a structured grid, representing the raw values a design system depends on

Design System Utility Tokens

A Practical Implementation Guide

Introduction

Series Context

This article is part of a broader design system series. It builds on ideas introduced in Theming Foundations and Primitives, but stands on its own.

A design system gives the developer sheet music to play from, complete with tempo, scores, and notation. Without it, every component improvises.

Design tokens are the score that keeps everything in rhythm, playing in time, and creating harmony across spacing, type, and layout.

Spacing — controlling rhythm

Spacing is the beat that holds a UI together. Every component — every padding, gap, and margin decision — reaches for spacing tokens.

Without a defined scale, the beat breaks down. A button gets 0.5rem padding, while a card gets an off-beat 14px. Each decision seems locally reasonable, but is globally inconsistent.

This system introduces a shared scale that covers the full range of layout needs.

spacing.css
:root {
    --spacing-xs:   0.25rem;    /*  4px */
    --spacing-sm:   0.5rem;     /*  8px */
    --spacing-md:   0.75rem;    /* 12px */
    --spacing-lg:   1rem;       /* 16px */
    --spacing-xl:   1.5rem;     /* 24px */
    --spacing-xxl:  2rem;       /* 32px */
    --spacing-3xl:  4rem;       /* 64px */
}

--spacing-xs

--spacing-sm

--spacing-md

--spacing-lg

--spacing-xl

--spacing-xxl

--spacing-3xl

Each block's width and height represents the corresponding spacing token value.

Why these values?

The scale is built on increments of 0.25rem (4px) — small enough to be precise, large enough to be meaningful.

Three t-shirt illustrations in ascending size labelled sm, md, and lg, each showing their corresponding rem value

Each step is named using t-shirt sizing ( sm, md, lg etc. ) rather than numeric values, so names communicate scale at a glance rather than requiring mental conversion.

A musical scale of intensity — pianissimo to fortissimo — would be a natural fit here. But t-shirt sizing (sm, md, lg, xl) is so widely adopted across design systems that familiarity wins.

Where spacing tokens live in the system

Spacing tokens are consumed at every layer:

  • Primitives use them for gap values between children
  • Components use them for internal spacing ( margins, padding, gaps, and offsets )

From there, utility classes expose the same scale as composable helpers for one-off spacing needs.

The utility layer uses the same t-shirt sizing as the tokens - .gap-lg and --spacing-lg are clearly the same step in that scale.

/* gap.css */
.gap-sm { gap: var(--spacing-sm); }

/* padding.css */
.p-sm { padding: var(--spacing-sm); }

/* margin.css */
.m-sm { margin: var(--spacing-sm); }

/* ...full reference in resources */

Typography Scale — controlling readability

A type scale is like a musical key — everything plays in the same register. Without it, headings drift to slightly different sizes, line heights clash, and labels feel just a little too large. The composition breaks down quietly, one decision at a time.

Font sizes

Each token uses clamp() to scale fluidly between a minimum and maximum value. Type stays readable at small viewport widths without becoming overwhelming at large ones — no media queries needed.

typography.css
:root {
    --font-size-xs:   clamp(0.75rem,  0.7rem  + 0.25vw, 0.875rem);
    --font-size-sm:   clamp(0.875rem, 0.8rem  + 0.375vw, 1rem);
    --font-size-base: clamp(1rem,     0.9rem  + 0.5vw,   1.125rem);
    --font-size-lg:   clamp(1.125rem, 1rem    + 0.625vw, 1.25rem);
    --font-size-xl:   clamp(1.25rem,  1rem    + 0.75vw,  1.5rem);
    --font-size-xxl:  clamp(1.4rem,   1rem    + 1vw,     1.875rem);
    --font-size-3xl:  clamp(1.75rem,  1.25rem + 1.375vw, 2.25rem);
    --font-size-4xl:  clamp(1.85rem,  1.25rem + 1.75vw,  3rem);
    --font-size-5xl:  clamp(1.9rem,   1.5rem  + 2.5vw,   3.75rem);
    --font-size-6xl:  clamp(2rem,     1.5rem  + 3.75vw,  4.5rem);
}

clamp(min, fluid, max) — the fluid value grows with the viewport, bounded by a minimum and maximum. The result is a type scale that adapts without breakpoints.

Line heights

Line heights are defined separately, giving the system independent control over vertical rhythm.

:root {
    --line-height-tight:       1;
    --line-height-snug:        1.1;
    --line-height-normal:      1.2;
    --line-height-relaxed:     1.3;
    --line-height-loose:       1.4;
    --line-height-extra-loose: 1.5;
    --line-height-super-loose: 1.6;
}

Large display text needs tighter leading. Body text needs more room to breathe.

Every UI has a score

Every token has a role

--line-height-tight

Spacing keeps the beat in time

Type gives every line its rhyme

--line-height-snug

When the scale is lost, the beat goes astray

The spacing guesses, the type goes its own way

--line-height-super-loose
Each block pairs a font size with its corresponding line height token — showing how leading changes with scale.

The global baseline

A single font-size and line-height on body sets the key. The type scale tokens then give precise control where it matters — headings, labels, badges, and helper text.

body {
    font-size: var(--font-size-base); 
    line-height: var(--line-height-super-loose);
}

Not all elements inherit font-size and line-height from body. Form elements — input, button, textarea, and select — use browser defaults unless explicitly reset. A CSS reset handles this.

See the Resources section for recommended starting points.

How the scale is consumed

Font size and line height tokens are paired into utility classes. The pairing is opinionated — smaller text gets more line height, larger text gets less.

/* sizes.css */
.text-xs   { font-size: var(--font-size-xs);   line-height: var(--line-height-loose); }
.text-sm   { font-size: var(--font-size-sm);   line-height: var(--line-height-extra-loose); }
.text-base { font-size: var(--font-size-base); line-height: var(--line-height-super-loose); }
.text-lg   { font-size: var(--font-size-lg);   line-height: var(--line-height-extra-loose); }
.text-xl   { font-size: var(--font-size-xl);   line-height: var(--line-height-loose); }

The exception is .text-xs — at very small sizes, loose leading creates too much distance between lines. A tighter value keeps small text cohesive.

Breakpoints — controlling adaptation

Breakpoints are the dynamic markings — they tell the layout when to shift register. Like a musical arrangement that changes feel at the chorus, a layout should also adapt at deliberate, shared moments.

If one component shifts at 768px and another at 769px, the result is a layout that feels unpolished. Shared breakpoints ensure every adaptation happens in step.

breakpoints.css
:root {
    --bp-xs:         18.75rem;  /*  300px */
    --bp-sm:         35rem;     /*  560px */
    --bp-md:         48rem;     /*  768px */
    --bp-lg:         75rem;     /* 1200px */
    --bp-xl:         90rem;     /* 1440px */
    --bp-characters: 80ch;      /* Maximum content line length */
}

Why rem?

Breakpoints defined in rem stay proportional to the type scale. When a user's font size increases, text takes up more space — a rem-based breakpoint shifts the layout at the right moment relative to that text.

The character width breakpoint

--bp-characters is different from the others — it is not a viewport breakpoint but a content constraint. Setting max-width: var(--bp-characters) on text content keeps line lengths within a readable range, regardless of the viewport width.

This is applied to <p> elements throughout the system.

A line that stretches the full width of a wide viewport becomes difficult to read — the eye loses its place, tracking back to the start of the next line.

Using breakpoints in CSS

CSS custom properties cannot be used directly inside @media queries — media queries do not inherit from :root. Instead, breakpoint tokens define shared values that are referenced consistently:

/* Use the raw value inside media queries */
@media screen and (min-width: 35rem) {
    .nav__wrapper {
        flex-direction: row;
    }
}

/* Use the token for max-width constraints in layout */
.layout-wrapper {
    max-width: var(--bp-lg);
}

The tokens remain the single source of truth.

Summary

These three token categories form the remaining foundation of the system.

  • Spacing ensures the beat holds — every gap, padding, and margin comes from a shared scale.
  • Typography ensures the composition is intentional — font sizes and line heights communicate hierarchy, not guesswork.
  • Breakpoints ensure the arrangement shifts in step — layout boundaries are shared contracts, not scattered magic numbers.

Together with the color and theme tokens from the theming post, the token layer is now complete. Every value the system needs — colors, spacing, typography, and layout have defined homes.

At this point, the system is no longer a collection of components — it is a score that every component plays from.

This is the last post in the design system series for now. The next series covers individual components — starting with the Button.

Code & Resources

This section documents constraints, tradeoffs, and reference material — it does not introduce new concepts.

Notes & Tradeoffs

  1. CSS custom properties cannot be used in media queries. This is a limitation of native CSS — media queries evaluate before the cascade, so custom properties have no value at that point.

    While preprocessors such as Sass and PostCSS with postcss-custom-media allow variables in media queries by compiling them to static values at build time, this system uses native CSS, so breakpoint tokens must be consumed as raw values inside @media rules.

  2. Fluid typography
    • The minimum value must be set in rem, not px. If someone increases their default font size, px values won't scale with it. rem values will.
    • Fluid typography can cause sites to fail WCAG SC 1.4.4 which requires that the type can be scaled upwards of 200% when zoomed in.

      See Addressing Accessibility Concerns With Using Fluid Type — Maxwell Barvian for a thorough treatment of the issue.

  3. T-shirt size naming over numeric values. This system uses t-shirt sizing (sm, md, lg) so token names and utility classes share the same language ( .gap-lg and --spacing-lg are clearly the same step in that scale. ).

    Whichever convention you choose, commit to it early — mixing naming systems creates exactly the inconsistency tokens are meant to prevent.

Further Reading

Spacing & Layout

Typography

CSS Reset

The global baseline defined in this post assumes browser defaults have already been normalised. Without a reset, default browser styles for margins, padding, and font rendering will interfere with the token layer.

Breakpoints

Token Reference

:root {
    --spacing-xs:  0.25rem;  /*  4px */
    --spacing-sm:  0.5rem;   /*  8px */
    --spacing-md:  0.75rem;  /* 12px */
    --spacing-lg:  1rem;     /* 16px */
    --spacing-xl:  1.5rem;   /* 24px */
    --spacing-xxl: 2rem;     /* 32px */
    --spacing-3xl: 4rem;     /* 64px */
}