Home / Snippets / Color & Theming /

Color scale tokens

A five-scale token system — primary, neutral, success, danger, and warning — each with nine lightness steps generated in oklch(). Change a hue angle, repaint everything.

Primary
Neutral
Success
Danger
Warning
Widely Supported
colortokensno-js

Quick implementation

:root {
  /* — Primary (purple) ——————————————————————— */
  --color-primary-50:  oklch(0.95 0.05 265);
  --color-primary-100: oklch(0.89 0.09 265);
  --color-primary-200: oklch(0.82 0.13 265);
  --color-primary-300: oklch(0.73 0.17 265);
  --color-primary-400: oklch(0.63 0.20 265);
  --color-primary-500: oklch(0.52 0.22 265); /* base */
  --color-primary-600: oklch(0.43 0.22 265);
  --color-primary-700: oklch(0.35 0.20 265);
  --color-primary-900: oklch(0.22 0.12 265);

  /* — Neutral ———————————————————————————————— */
  --color-neutral-50:  oklch(0.97 0.005 260);
  --color-neutral-100: oklch(0.93 0.008 260);
  --color-neutral-200: oklch(0.87 0.010 260);
  --color-neutral-300: oklch(0.78 0.012 260);
  --color-neutral-400: oklch(0.65 0.013 260);
  --color-neutral-500: oklch(0.50 0.013 260);
  --color-neutral-600: oklch(0.40 0.010 260);
  --color-neutral-700: oklch(0.30 0.008 260);
  --color-neutral-900: oklch(0.15 0.005 260);

  /* — Success (green) ————————————————————————*/
  --color-success-50:  oklch(0.96 0.04 145);
  --color-success-100: oklch(0.90 0.08 145);
  --color-success-200: oklch(0.83 0.12 145);
  --color-success-300: oklch(0.74 0.16 145);
  --color-success-400: oklch(0.65 0.18 145);
  --color-success-500: oklch(0.58 0.20 145); /* base */
  --color-success-600: oklch(0.48 0.19 145);
  --color-success-700: oklch(0.38 0.17 145);
  --color-success-900: oklch(0.22 0.10 145);

  /* — Danger (red) ———————————————————————————*/
  --color-danger-50:   oklch(0.96 0.04 25);
  --color-danger-100:  oklch(0.90 0.08 25);
  --color-danger-200:  oklch(0.83 0.13 25);
  --color-danger-300:  oklch(0.73 0.17 25);
  --color-danger-400:  oklch(0.63 0.20 25);
  --color-danger-500:  oklch(0.56 0.22 25);  /* base */
  --color-danger-600:  oklch(0.46 0.22 25);
  --color-danger-700:  oklch(0.37 0.20 25);
  --color-danger-900:  oklch(0.22 0.12 25);

  /* — Warning (amber) ————————————————————————*/
  --color-warning-50:  oklch(0.97 0.04 75);
  --color-warning-100: oklch(0.92 0.08 75);
  --color-warning-200: oklch(0.86 0.13 75);
  --color-warning-300: oklch(0.78 0.17 75);
  --color-warning-400: oklch(0.70 0.19 75);
  --color-warning-500: oklch(0.60 0.18 75);  /* base */
  --color-warning-600: oklch(0.50 0.16 75);
  --color-warning-700: oklch(0.40 0.14 75);
  --color-warning-900: oklch(0.25 0.09 75);

  /* — Semantic roles ———————————————————————— */
  --color-bg:           var(--color-neutral-50);
  --color-text:         var(--color-neutral-900);
  --color-text-muted:   var(--color-neutral-500);
  --color-border:       var(--color-neutral-200);
  --color-link:         var(--color-primary-500);
  --color-link-hover:   var(--color-primary-400);
}

Prompt this to your LLM

Includes role, constraints, framework variants, and edge cases.

You are a senior design-systems engineer building a multi-scale
CSS color token system using oklch().

Goal: Generate five named color scales — primary, neutral, success,
danger, and warning — each with nine steps (50, 100, 200, 300, 400,
500, 600, 700, 900). Add semantic role aliases that reference the
scale tokens.

Technical constraints:
- Use oklch(L C H) for every color value — no hex, hsl, or rgb.
- Each scale uses a fixed hue angle and chroma with only the
  lightness varying across steps.
- Reduce chroma for tints (steps 50–200) and deep shades (900)
  using calc() so the extremes stay subtle rather than washed-out.
- Neutral scale uses near-zero chroma (0.005–0.015) for cool greys.
- Warning (amber, hue ~75) uses dark text on light and mid steps
  because yellow-range colors never achieve sufficient contrast
  with white text at high lightness.
- Semantic aliases must reference scale tokens, not hard-coded
  values: --color-bg, --color-text, --color-text-muted,
  --color-border, --color-link, --color-link-hover,
  --color-surface-success, --color-surface-danger, etc.

Scales to generate:
- Primary: hue 265 (purple), chroma 0.22
- Neutral: hue 260, chroma 0.005–0.013
- Success: hue 145 (green), chroma 0.20
- Danger: hue 25 (red), chroma 0.22
- Warning: hue 75 (amber), chroma 0.18

Dark-mode variant:
Provide a [data-theme="dark"] override that flips the semantic
aliases (--color-bg → neutral-900, --color-text → neutral-50, etc.)
while reusing the same scale tokens.

Return only CSS custom properties on :root and [data-theme="dark"].

Why five scales and nine steps

Most design systems need exactly these five roles: a brand color for interactive elements, a neutral for text and borders, and three semantic states (success, danger, warning) for feedback. Anything beyond this tends to be a variant or alias, not a new scale.

The nine-step numbering (50–900) mirrors the Tailwind convention that most engineers already understand. Step 500 is the "base" — the closest to what you'd call the pure color. Steps below 500 are tints suitable for backgrounds and surfaces; steps above 500 are shades for hover states, borders, and dark-mode text. You don't have to use every step — define them all and only reference the ones you need.

How oklch keeps scales perceptually even

In hsl(), equal lightness steps look uneven across hues — yellows appear much brighter than blues at the same L value. OKLCH is calibrated to human vision, so equal L increments produce equal perceived brightness shifts regardless of hue. This means the nine lightness values in the snippet above generate a visually consistent scale for every color, including amber and green where hsl() notoriously fails. No manual tweaking of individual steps.

The chroma reduction at the extremes (calc(var(--brand-c) * 0.35) at step 50, similar at step 900) prevents two common problems: step 50 going neon-pale with full chroma at high lightness, and step 900 going muddy with full chroma at near-black lightness. Mid-range steps 300–700 carry the full chroma where brand character matters most.

Semantic aliases over raw tokens

Components should never reference numbered scale tokens directly. A button should not use var(--color-primary-500) — it should use var(--color-link) or a component-specific alias like var(--btn-bg). The semantic layer decouples components from specific scale stops. When the design team decides the default link color should shift from step 500 to step 400 for better contrast, only one alias changes — not every component that uses the color.

The dark-mode flip is the clearest demonstration of this: in light mode --color-bg maps to --color-neutral-50; in dark mode it maps to --color-neutral-900. Every component that references --color-bg flips correctly with a single override on [data-theme="dark"].