Ramp & token generator

One brand colour to eleven steps, with roles assigned by measurement.

yours

50

100

200

300

400

500

600

700

800

900

950

Light theme

  • Aa--brand-text600Body copy on the page background5.67:1
  • Aa--brand-text-large500Headings 24px and above3.95:1
  • Aa--brand-border500Input borders and other controls that need to be findable3.95:1
  • Aa--brand-surface50Tinted background blocks—

Chosen by measuring each step against its requirement, not by eye. Your brand colour is usually not your text colour.

Dark theme

  • Aa--brand-text500Body copy on the page background5.30:1
  • Aa--brand-text-large600Headings 24px and above3.69:1
  • Aa--brand-border600Input borders and other controls that need to be findable3.69:1
  • Aa--brand-surface950Tinted background blocks—

Chosen by measuring each step against its requirement, not by eye. Your brand colour is usually not your text colour.

:root {
  --brand-50: #f1f6ff;
  --brand-100: #e0ebff;
  --brand-200: #c2d8ff;
  --brand-300: #9bbeff;
  --brand-400: #6a9cff;
  --brand-500: #3978ff;
  --brand-600: #225ddf;
  --brand-700: #1648b5;
  --brand-800: #0f3589;
  --brand-900: #0b255f;
  --brand-950: #05153c;
}

What colour ramp is

A ramp is one hue expressed as an ordered series of lightness steps, usually numbered 50 to 950, from which a design system draws its text, border and surface values. A token is the name a component references instead of the raw value.

What this tool does that others do not

  • Eleven steps, stepped in OKLCH

    An HSL ramp built on even lightness steps does not look even, because HSL lightness is a transform of RGB with no model of vision in it. OKLCH lightness is perceptually uniform, so equal steps look equal — which is the entire point of a ramp. Chroma is tapered at both ends so the lightest and darkest steps do not go out of gamut.

    The scale is 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950 — the convention most systems and Tailwind already use, with 950 present because dark themes need a surface below 900.

  • Roles assigned by measurement

    Which step is text, which is border, which is surface is decided by reading the contrast of each step against each ground, not by convention. A vivid brand colour is built to be seen at size on a logo; as 16px body copy it usually misses 4.5:1, and the step two or three darker still reads as the brand.

  • Dark roles derived, not inverted

    Inverting the light ramp is the most common mistake. The WCAG 2 formula adds a fixed constant that dominates at the dark end, so inverted pairs can post passing ratios while reading as dim, and saturated colours vibrate against dark backgrounds. The dark roles are assigned separately from the same ramp.

  • Output as roles, not values

    Components should reference --brand-text, never --brand-600. Regenerate the ramp when the brand colour changes and the roles keep pointing at the right steps; reference raw values and every component has to be revisited. It is also what makes theming possible at all, because a role can point at a different step per theme and a raw value cannot.

How to use it

  1. Enter the brand colour exactly as given to you

    It is the anchor, not the output. Where it lands on the scale is a result rather than a decision.

  2. Read the roles before the swatches

    The useful output is the assignment — this step is text, this step is border — with the ratio beside it.

  3. Check both themes

    Dark roles come from the same ramp but are chosen separately, so read them as their own set rather than assuming symmetry.

  4. Export as CSS variables, Tailwind config or JSON tokens

    Whichever your build already reads. The code panel names colours by step (brand-500); copy the roles separately if your build wants names like text or border.

Worked example

Real input, real figures.

A brand colour that cannot be the text colour

You give it

A vivid mid-lightness brand blue, entered as the anchor.

You get

It lands near the middle of the scale and fails 4.5:1 as 16px text on white. The role assignment points --brand-text two or three steps darker, where the ratio clears and the hue is unchanged, so the text still reads as the brand.

Against a typical shade generator

AspectThis toolA typical generator
Interpolation spaceOKLCH, perceptually uniformHSL or hex mixing
Base colour placementMarked at the nearest stepForced onto 500
Role assignmentBy measured contrast, per themeBy convention, or none
Dark modeDerived separately from the same rampThe light ramp reversed
ExportCSS variables, Tailwind config, JSON tokensA list of hex codes

Guide

Why OKLCH and not HSL

An HSL ramp built on even lightness steps does not look even. HSL lightness is a mathematical transform of RGB with no model of human vision in it, so 50% means something very different for yellow than for blue.

OKLCH lightness is perceptually uniform. Equal steps look equal, which is the entire point of a ramp.

Roles, not values

Components should reference --brand-text, never --brand-600. When the brand colour changes — and it will — you regenerate the ramp and the roles keep pointing at the right steps. Reference raw values and every component has to be revisited.

It is also what makes theming possible at all. A role can point at a different step in each theme. A raw value cannot.

Questions people ask

Straight answers. Each one links directly: copy the address after opening it.

Why is my brand colour not the text colour?

A vivid brand colour is built to be seen at size, on a logo. Set as 16px body copy it usually misses 4.5:1. The step two or three darker still reads as your brand and actually passes, which is why roles here are assigned by measurement rather than by eye.

Why eleven steps?

It matches the convention most design systems and Tailwind already use, so the output drops into an existing setup without renaming. The extra 950 step exists because dark themes need a surface darker than 900.

Can I invert the light ramp for dark mode?

No, and this is the most common mistake. The WCAG 2 formula adds a fixed constant that dominates at the dark end, so inverted pairs can post passing ratios while reading as dim. Saturated colours also vibrate against dark backgrounds. The dark roles here are assigned separately from the same ramp.