Home / Snippets / Color & Theming /

Monochrome palette

A perceptually uniform gray scale using oklch() with zero chroma — no tint bias, no JavaScript.

oklch(
0.95 0 0)
oklch(
0.85 0 0)
oklch(
0.75 0 0)
oklch(
0.65 0 0)
oklch(
0.55 0 0)
oklch(
0.45 0 0)
oklch(
0.35 0 0)
oklch(
0.25 0 0)
oklch(
0.15 0 0)
Text on light

High-contrast body text using oklch(0.15 0 0) on a near-white background.

Card with border

Dark card background with a visible border and light muted text.

Mid-tone surface

A balanced mid-gray that works for disabled states and dividers.

Widely Supported
colorno-js

Quick implementation

:root {
  --gray-50:  oklch(0.95 0 0);
  --gray-100: oklch(0.90 0 0);
  --gray-200: oklch(0.85 0 0);
  --gray-300: oklch(0.75 0 0);
  --gray-400: oklch(0.65 0 0);
  --gray-500: oklch(0.55 0 0);
  --gray-600: oklch(0.45 0 0);
  --gray-700: oklch(0.35 0 0);
  --gray-800: oklch(0.25 0 0);
  --gray-900: oklch(0.15 0 0);
}

Prompt this to your LLM

Includes role, constraints, two framework variants, and edge cases to handle.

You are a senior frontend engineer building a design token system.

Goal: Create a 10-stop monochrome gray scale using oklch() with zero
chroma — no blue, red, or warm tint bias — just pure neutral grays at
evenly spaced perceptual lightness values.

Technical constraints:
- Use oklch(L 0 0) for every stop. Zero chroma means the hue angle has
  no effect; the result is always a pure achromatic gray.
- Name the custom properties --gray-50 through --gray-900 to match the
  Tailwind gray naming convention (50 = lightest, 900 = darkest).
- Step lightness evenly: 0.95, 0.90, 0.85, 0.75, 0.65, 0.55, 0.45,
  0.35, 0.25, 0.15.
- Define all tokens as custom properties on :root so they cascade
  everywhere.
- Use oklch() — no hex, no hsl, no rgb values.

Framework variant (pick one):
A) Plain CSS custom properties on :root.
B) JavaScript ES module that exports a theme object with gray-50 through
   gray-900 keys mapping to oklch() strings; suitable for CSS-in-JS or
   Tailwind config.

Edge cases to handle:
- oklch(L 0 0) with L = 0 is absolute black; L = 1 is absolute white.
  Do not use 0 or 1 directly — leave headroom (0.05 and 0.97) for
  shadows and highlights that need to go darker/lighter.
- When building light-mode and dark-mode themes from the same scale,
  map semantic tokens (--color-surface, --color-on-surface) to scale
  stops rather than hard-coding lightness values, so you can swap modes
  by reassigning a handful of semantic tokens.
- If a warm or cool tint is desired later, only chroma needs to be
  raised (e.g. oklch(0.55 0.02 265) for a cool blue-gray). The scale
  structure stays the same.

Return CSS only (or a JS module if variant B is chosen).

Why oklch(L 0 0) gives truly even grays

In older color spaces like hsl and rgb, grays defined at equal lightness intervals look uneven to the human eye — some steps feel like big jumps, others feel nearly identical. This is because HSL lightness is not perceptually uniform. oklch is built on the Oklab perceptual model, which is specifically designed so that equal numeric changes in lightness correspond to equal changes in perceived brightness.

Setting chroma to 0 removes all color from the value entirely. The hue channel becomes irrelevant — oklch(0.55 0 0) and oklch(0.55 0 180) are the same achromatic gray. This makes the notation explicit: when you see chroma zero, you know the intention is a pure neutral.

The result is a scale where every step looks the same "distance" apart — gray-200 to gray-300 feels the same as gray-700 to gray-800. This predictability is what makes a monochrome scale genuinely useful for building interfaces rather than just aesthetically pleasing.

Mapping to gray-50 through gray-900

The naming mirrors Tailwind CSS's gray scale convention, which has become the de facto standard for design token naming in web projects. The key difference is that Tailwind's grays carry a slight cool-blue tint (they use hsl internally), while this scale is genuinely neutral at zero chroma.

The lightness values map roughly as follows: --gray-50 at 0.95 is your near-white page background in light mode; --gray-900 at 0.15 is your near-black page background in dark mode. The middle stops — --gray-400 through --gray-600 — are ideal for disabled states, dividers, and placeholder text.

Because the scale is perceptually even, you can safely pick any two adjacent stops for a foreground/background pair and know the contrast ratio will be meaningful. For accessibility compliance (WCAG AA, 4.5:1 for body text), pair stops that are at least three to four steps apart: --gray-800 on --gray-50, or --gray-50 on --gray-800.

Adding warm or cool tints

A pure neutral gray scale is a strong foundation, but many design systems prefer a slight temperature. A warm gray has a subtle yellow-orange tint; a cool gray leans blue or purple. In oklch, you add temperature by raising the chroma slightly and choosing a hue.

For a cool blue-gray, raise chroma to around 0.010.03 and set hue to the 240–270 range: oklch(0.55 0.02 265). For a warm gray, use a hue around 50–80: oklch(0.55 0.02 65). The rest of the scale structure stays identical — only the chroma and hue values change.

Because you are working in a perceptually uniform space, this tint stays consistent across all lightness levels. In HSL, adding a tint to dark grays often over-saturates them; in oklch, the chroma is bounded by the actual gamut at each lightness level, so you get a gentle, even tint throughout the full range.