Demo type · 05

Design-system page — swatches, naming table, do & don’t

Use this when you’re teaching the conventions of a system: the named colour and spacing tokens, how the names are built, and the right vs wrong way to use them.

This is a copyable exemplar. Lift the demo <section>s below into a lesson built from assets/lesson-template.html — keep the design tokens and the warm-neutral palette verbatim.

Read the plain version, or open the technical layer on any section.
1

The big idea


A design system is a small shared dictionary for a product’s look. Instead of writing a raw colour like #D97757 in fifty places, you give it one agreed name — --clay — and everyone uses that name. Change the name’s value once and every button, link, and badge updates together.

This page teaches the three habits a design system lives or dies by: the swatches (what the named values actually are), the naming table (how a token’s name is built so anyone can guess the right one), and the do / don’t pairs (the small mistakes that quietly break the system).

Think of it like… a kitchen with labelled jars. Nobody scoops from an unmarked bag of white powder hoping it’s sugar — they reach for the jar that says “Sugar.” Tokens are the labels; this page is the rule that says always reach for the jar, never the raw bag.

Under the hood

Tokens here are CSS custom properties declared once on :root and consumed with var(--token). Because cascade resolution is live, overriding a token on a scope (a theme class, prefers-color-scheme, a component root) re-themes every descendant with zero markup changes — the single source of truth is the declaration, not the usage site.

Two layers are worth separating in a mature system: primitive tokens (raw scale values like --clay: #D97757) and semantic tokens that alias them by role (--btn-bg: var(--clay)). Components consume only semantic tokens, so re-skinning means re-pointing the alias, never touching the component.

2

The swatches — colour & spacing tokens


Every named token, shown as a swatch with its real value. This is the page a designer or developer scans to find “the right orange” instead of guessing a hex code.

Colour tokens

--ivory#FAF9F5 · page bg
--slate#141413 · ink
--clay#D97757 · primary
--clay-d#B85C3E · primary hover
--olive#788C5D · success
--sky#5C7CA3 · info
--rust#B04A3F · danger
--oat#E3DACC · subtle fill
--gray-100#F0EEE6 · muted bg
--gray-500#87867F · faint text

Spacing scale

--space-14px · hairline gap
--space-28px · tight
--space-312px · control padding
--space-416px · default
--space-624px · between blocks
--space-1252px · section gap

Why a scale, not arbitrary numbers

The spacing tokens follow a fixed step scale (a 4px base: 4 · 8 · 12 · 16 · 24 · 52). Constraining spacing to a scale removes the “is it 13px or 14px?” bikeshed and guarantees vertical rhythm — every gap in the product is a multiple you can reason about. Colours are similarly closed: ten names, no off-roster hexes.

Declaring them

All of these live in one :root block at the top of the stylesheet, e.g. --clay:#D97757; and --space-4:16px;. A component never restates a hex — it writes color:var(--clay) or padding:var(--space-3) var(--space-4).

3

How a token name is built


A good token name reads like a sentence: category, then role, then state. Once you know the shape, you can guess a token you’ve never seen.

--btn-bg-hover btn category bg role hover state (optional) resolves to var(--clay-d)
Read left → right: category btn, role bg, state hover → resolves to a primitive token.
4

The naming table — token → value → usage


The contract in one table. Each row is a semantic token: its name, the primitive it points to, and exactly where to reach for it.

TokenValueUse it for
--btn-bg
category · role
var(--clay) Fill of a primary button — the one main action on a screen.
--btn-bg-hover
category · role · state
var(--clay-d) Primary button background while the pointer is over it.
--btn-fg
category · role
#FFFFFF Label text on a filled primary button.
--btn-bg-success
category · role · intent
var(--olive) Confirm / save actions where green signals “safe to proceed”.
--btn-bg-danger
category · role · intent
var(--rust) Destructive actions only — delete, remove, cancel-for-real.
--space-3
scale step
12px Vertical padding inside a control. Pairs with --space-4 horizontally.
--radius-row
category · role
9px Corner radius on buttons, inputs, chips — anything row-sized.

Semantic over primitive at the call site

Notice every --btn-* token aliases a primitive (--clay, --olive…). Components reference --btn-bg, never --clay directly. That indirection is what lets you ship a “high-contrast” or “dark” theme: you re-point the seven --btn-* aliases on a scope and the markup is untouched.

styles/tokens.css
/* primitive scale */
--clay:   #D97757;
--clay-d: #B85C3E;

/* semantic aliases — components use THESE */
--btn-bg:       var(--clay);
--btn-bg-hover: var(--clay-d);
--btn-fg:       #FFFFFF;
5

Do & don’t


The same button, two ways. The left column keeps the system intact; the right column quietly breaks it.

Do — reference the token

Use the named token. If the brand orange ever changes, this button changes with it — for free.

.btn-primary {
  background: var(--btn-bg);
  color:      var(--btn-fg);
  padding:    var(--space-3) var(--space-4);
  border-radius: var(--radius-row);
}
Don’t — hard-code the value

A raw hex and magic numbers. Invisible to a theme change, and the next person can’t tell which orange this is meant to be.

.btn-primary {
  background: #D97757;   /* which orange? */
  color:      #fff;
  padding:    11px 15px;  /* off-scale */
  border-radius: 8px;     /* not 9px */
}
Do — name by role

The danger token says what it’s for. Anyone reading the code knows this button deletes something.

/* delete confirmation */
.btn-delete {
  background: var(--btn-bg-danger);
}
Don’t — name by appearance

Named after the colour, not the job. Rename the brand red and the token --red-button becomes a lie.

/* what is this for? */
.btn-delete {
  background: var(--red-button);
}
6

Try it — change a token, watch the button


Pick an intent and a size below. The preview button is styled only from tokens — the code readout updates to show the exact tokens in play.

Intent

Size

resolved tokens
.btn { }

How the preview is wired

The button carries no per-intent CSS class. Each segmented control writes the semantic tokens (--btn-bg, --btn-px, …) onto the element’s inline style as custom properties. The single .ds-btn rule reads them with var(). That is exactly how a real token-driven component behaves: swap the token values, the component re-renders, no new selectors.