Intermediate · Studio Colour
Knowing colour theory is one thing; turning a single brand colour into a working UI palette is another. This is the intermediate step between “I have a nice purple” and “every button, border, surface and state has a defined, accessible colour.” We’ll build a full ramp from one base, tint the neutrals, add semantic colours, wire it to tokens, and make dark mode fall out for free.
From one colour to a scale
Start with your base at the middle of a 50–900 scale (500). Build lighter tints above it and darker shades below. The trick most people miss: step by perceived lightness, not by evenly adding white and black — otherwise the light end bunches up and the dark end falls off a cliff.
The clean way to do this is in OKLCH, where the L channel is perceived lightness. Keep hue and chroma roughly constant and walk L in even steps:
/* Even perceived steps: only L changes */
--p-50: oklch(0.97 0.03 285);
--p-500: oklch(0.55 0.20 285); /* your base */
--p-900: oklch(0.25 0.10 285);Notice chroma tapers at the extremes — very light and very dark colours can’t hold as much saturation, so easing it off keeps the ramp believable.
Don’t forget the neutrals
A brand ramp alone can’t build a UI — you need greys for text, surfaces and borders. But pure grey next to a saturated brand looks dirty. Tint your neutrals with a whisper of the brand hue (chroma ~0.01–0.03) so they feel related:
--n-100: oklch(0.96 0.005 285); /* barely purple grey */
--n-700: oklch(0.40 0.015 285);Assign roles, not just values
A palette becomes a system when each step has a job: 50–100 for backgrounds and hovers, 200–300 for borders and dividers, 500 for primary actions, 600–700 for hover/active states, 800–900 for text on light. Name them by role so swapping the brand colour later re-themes everything:
--surface: var(--n-50);
--border: var(--p-200);
--primary: var(--p-500);
--primary-hv: var(--p-600);
--text: var(--n-900);Components reference the role tokens, never the raw ramp. That one layer of indirection is what makes a re-theme a five-minute job instead of a find-and-replace across the codebase.
Add semantic colours
Success, warning, and danger aren’t decoration — they carry meaning, so they need their own small ramps (green, amber, red) built the same perceptual way. Match their L at each step to the brand ramp so a green button and a purple button feel like the same weight.
Apply the 60-30-10 rule
Dominant/neutral 60%, secondary 30%, accent 10%. Your scale supplies all three: a near-neutral for the 60, a mid tone for the 30, and the saturated 500 as the 10% accent that draws the eye to actions. If everything is branded, nothing stands out — the discipline is what makes the accent read as “click here.”
Dark mode for free
Because the ramp is defined by perceived lightness, dark mode is mostly a matter of flipping which end you pull from. Surfaces move to 900–800, text to 100–200, and the primary often shifts up a step or two so it stays vivid on a dark surface. Redefine the role tokens inside a prefers-color-scheme block and every component follows — no per-component overrides.
Check every pair for contrast
A system is only usable if the text-on-surface pairings pass. 700–900 on 50–100 is safe; 400 on white almost never is. Lock the accessible pairings before you ship the tokens — aim for WCAG 4.5:1 on body text and 3:1 on large text and UI borders. Build the contrast check into the ramp, not as an afterthought, and you’ll never ship an unreadable state.
Common mistakes
- Stepping by equal RGB amounts. The light end bunches and the dark end collapses. Step by perceived lightness instead.
- Pure-grey neutrals. They look muddy beside a saturated brand. Tint them with a trace of the brand hue.
- Hard-coding ramp values in components. Reference role tokens so a re-theme stays trivial.
- Full chroma at the extremes. Very light and very dark steps can’t hold saturation — ease it off.
Studio Colour spins a single base into a perceptually even tint-and-shade scale, tints your neutrals, checks every pair for contrast, and exports the lot as tokens. Free Chrome side panel.


