Guide · Design systems

Turning a palette into a design system: scales, tokens and CSS variables

Five nice colours are not a colour system. Here is how to get from a generated palette to a named, tested set of tokens your whole team can build against.

The gap between a palette and a system

A palette answers "which colours?". A colour system answers "which colour, where, and what happens when it is on a dark background, disabled, hovered or in an error state?". The second question is what actually gets asked fifty times a week once a project is underway, and answering it once — in tokens — is what stops a codebase accumulating eleven slightly different greys.

The move from one to the other has three steps: build tonal scales, name them by role, then ship them as variables.

Step 1: turn each colour into a scale

Take the palette out of the generator and pick the two or three colours doing real work — usually a brand colour, a neutral and an accent. Each becomes a scale of tints and shades, conventionally numbered 50 (lightest) to 900 (darkest), matching the convention Tailwind and Material both use.

The generator will do this for you: open the Tonal scale panel, choose which palette colour to build from, and you get all ten steps with copy buttons for CSS, SCSS or Tailwind. It works in OKLCH for the reason described below. If you would rather build a scale by hand, or want to understand what the panel is doing, read on.

The reliable way to build one: hold hue and saturation roughly steady and step the lightness. In HSL that is a single number changing:

--brand-50:  hsl(190 60% 96%);
--brand-100: hsl(190 58% 90%);
--brand-200: hsl(190 56% 80%);
--brand-300: hsl(190 55% 68%);
--brand-400: hsl(190 55% 60%);
--brand-500: hsl(190 55% 52%);   /* the colour you picked */
--brand-600: hsl(190 56% 43%);
--brand-700: hsl(190 58% 34%);
--brand-800: hsl(190 60% 26%);
--brand-900: hsl(190 62% 18%);

Two refinements make a scale look professional rather than mechanical. First, nudge saturation up slightly at the dark end and down at the light end — pale tints with full saturation look chalky. Second, if you are working in OKLCH, step the lightness there instead; because its lightness axis is perceptually uniform, evenly spaced numbers produce evenly spaced steps, which HSL cannot promise.

Both refinements are what the built-in Tonal scale panel applies: it converts your colour to OKLCH, steps the lightness across ten perceptually even targets, damps the chroma at the pale end, and then reduces chroma further wherever a step would fall outside the sRGB gamut — which keeps the hue steady instead of letting it drift as naive clipping would.

Check the scale by squinting at it. Every step should read as clearly different from its neighbours, with no two rungs collapsing into each other and no sudden jump in the middle.

Step 2: name by role, not by colour

This is the step teams skip and later regret. --teal-500 describes what a colour is; --color-action describes what it is for. When the brand changes — and it will — role names survive and hex-based names become lies.

The usual pattern is two layers:

:root {
  /* primitives */
  --teal-500: #3DB3CB;
  --teal-700: #24788B;
  --ink-900:  #0F2228;
  --ink-500:  #5A6B70;
  --paper-50: #EEF3F4;

  /* semantic */
  --color-surface:     #FFFFFF;
  --color-surface-alt: var(--paper-50);
  --color-text:        var(--ink-900);
  --color-text-muted:  var(--ink-500);
  --color-action:      var(--teal-500);
  --color-action-hover:var(--teal-700);
  --color-border:      #E1EAEC;
}

The payoff arrives at dark mode, when you re-point the semantic layer at different primitives and every component follows without a single edit. See designing a dark mode palette for how to choose those values.

Step 3: decide how many colours you actually need

A surprisingly small set covers most interfaces:

RoleTypical countNotes
Neutrals7–10 stepsThe workhorse. Backgrounds, borders, text, dividers. Give this the most steps.
Brand / action4–6 stepsDefault, hover, active, disabled, plus a pale tint for backgrounds.
Accent3–4 stepsOptional. Highlights, illustrations, data.
Feedback3 steps × 4Success, warning, danger, info — each needs a text, a background and a border tone.

If your palette has more than that, you have a mood board, not a system.

Step 4: test the pairs, not the swatches

A token is only usable if it passes contrast against the surfaces it will sit on. Before you lock the system, run the combinations you will actually ship through the contrast checker:

Record the passing pairs in the system's documentation. "Use --color-text-muted on --color-surface only, never on --color-surface-alt" is exactly the kind of rule that saves an accessibility audit later. The full detail is in our guide to WCAG contrast.

Step 5: ship it in the format your stack wants

The generator's export panel produces the same palette in the four formats most projects need:

Whichever you pick, keep one source of truth. A palette that lives in a CSS file, a Figma library and a Tailwind config independently will drift within a month; generate the others from one of them.

A pragmatic shortcut. Generate a five-colour palette, lock the two you are sure about, and export CSS variables. Expand only the neutral into a full scale on day one — most projects need ten greys long before they need ten brand tints.

Build your palette →

Related guides: HEX, RGB, HSL & OKLCH · Dark mode palettes · Accessible colour