Bhinish Dhiman· updated 5 October 2026

How to build an accessible palette from a single brand colour

Hold the hue, step the lightness evenly in OKLCH, and taper chroma at both ends. That gives you a ramp from one colour. Then assign roles by contrast requirement rather than by eye, and generate a separate lighter variant for dark mode.

Most brand guidelines hand you one hex code and a logo. Turning that into something you can build an interface with is a systematic job, and doing it by eye is where accessibility problems get baked in.

Step 1 — find the hue

Convert the brand colour to OKLCH and note the hue angle. That number is the brand. Everything else is adjustable.

#2563eb  →  oklch(54.6% 0.215 262.9)

Hue 262.9 is now fixed for the entire ramp. Lightness and chroma are the variables.

Step 2 — build the ramp

Ten steps, evenly spaced in lightness, with chroma peaking in the middle and tapering at both ends. Very light and very dark colours cannot sustain high chroma without leaving the displayable range.

--brand-50:  oklch(97% 0.015 263);
--brand-100: oklch(93% 0.035 263);
--brand-200: oklch(87% 0.070 263);
--brand-300: oklch(79% 0.120 263);
--brand-400: oklch(67% 0.180 263);
--brand-500: oklch(55% 0.215 263);  /* the original */
--brand-600: oklch(48% 0.200 263);
--brand-700: oklch(40% 0.165 263);
--brand-800: oklch(31% 0.120 263);
--brand-900: oklch(22% 0.075 263);

Because OKLCH lightness is perceptually uniform, these steps look even. The same exercise in HSL produces a ramp that bunches in the middle and jumps at the ends.

Step 3 — assign roles by measurement, not by eye

This is the step that determines whether the system is accessible, and it is the one most often done by intuition.

Do not pick “the nice one” for text. Work backwards from the contrast requirement:

Role Requirement Typical step
Body text on white 4.5:1 600–700
Large text on white 3:1 500–600
Button background with white label 4.5:1 for the label 600
Borders and dividers 3:1 if meaningful 400 (300 measures 1.95:1 on white, so decoration only)
Subtle backgrounds none 50–100

The useful insight: your brand colour is usually not your text colour. A vivid 500 is built to be seen at size, on a logo. Set it as 16px body copy and it will usually miss 4.5:1. The 600 or 700 step is what body text wants, and it is still recognisably your blue.

Step 4 — generate the dark-mode variants

Do not assume the ramp inverts. It does not, for two reasons.

The WCAG 2 formula adds a fixed flare constant that dominates at the dark end, so dark pairs can post passing ratios while reading as dim. And highly saturated colours vibrate against dark backgrounds in a way they do not against white.

So each semantic role needs a second value, lighter and usually less saturated:

:root {
  --text-brand: var(--brand-700);
  --surface-brand: var(--brand-50);
  --border-brand: var(--brand-400);
}

:root[data-theme='dark'] {
  --text-brand: var(--brand-300);
  --surface-brand: var(--brand-900);
  --border-brand: var(--brand-700);
}

Same ramp, different steps. This is why building the ramp first pays off — dark mode becomes a remapping rather than a second design exercise.

Step 5 — name things by role, not by value

--brand-600 is a value. --text-brand is a role. Components should reference roles.

The reason is practical: when the brand colour changes — and it will — you regenerate the ramp and the roles keep pointing at the right steps. If components reference --brand-600 directly, every one of them has to be revisited.

It also makes theming possible at all. A role can point at different steps in different themes. A raw value cannot.

Step 6 — check the pairs, not just the ramp

A ramp that passes contrast can still fail in use. Two things to verify:

Every text-on-surface combination you actually ship. Not every possible pair — the ones that appear in the interface. Body on page background, body on card, link on card, button label on button.

Whether any two colours collapse for colour-blind users. If your system includes a second accent — a success green alongside the brand blue — check that pair specifically. Contrast checking compares foreground to background and never foreground to foreground, so this failure passes every ratio audit.

The shortest version

Hold the hue. Step the lightness. Taper the chroma. Assign roles by measurement rather than by eye. Generate dark variants rather than inverting. Name by role.

Six steps and one brand colour gets you a system that passes, in both themes, without anyone having to negotiate with the brand guidelines.

Our contrast checker does the measurement half — give it a pair and it shows the ratio, the APCA figure at your real size and weight, and the nearest passing colour if it misses.