Skip to content
Back to list

One color is not a system

A brand guide that gives you one hex and says "use it often" has not given you a color system. It has given you a gap.

Buttons, washes, borders, charts, dark-mode surfaces, and the text on all of them cannot share one value. If you invent the rest as you go, you will end up with three different "light blues" in three files. None of them have a name. None of them are easy to check twice.

The reliable fix is a named 10-step palette built from that one color. Not a mood board. Not five extra hues from a color wheel. Ten steps in the same family, with names a designer and an engineer can share.

The scale most teams already use

Material, Tailwind, Carbon, and most token files use the same structure:

StepJob on a light surfaceJob on a dark surface
50Softest washNear-white text and thin highlights
100Card tint, hover fillSecondary text
200Subtle bordersQuiet borders
300Disabled fill, stronger borderMuted icons
400Placeholder, inactive iconSecondary icons
500Base. The brand as shippedBase, still
600Hover of 500Hover of 500
700Pressed, strong headingStrong heading
800High-contrast textRaised dark surface
900Darkest textDeepest surface

50 is the step closest to white. 500 is the color you started with, or the closest useful version of it. 900 is as dark as the hue can go before it reads as black.

The numbers are not lightness percents. They are roles. blue-500 means "the blue we mean." blue-50 means "that blue as a wash." Once the names exist, nobody has to describe a color in a pull request.

Why ten steps

Five steps are enough for an accent you barely use. Twelve steps are useful for a full gray scale. Ten covers:

  • a wash
  • a border
  • a disabled state
  • the base
  • a hover
  • a pressed state
  • text that must pass 4.5:1 on the wash
  • a dark surface in the same hue

That is a product color, not a single swatch.

If you later need 950 for a near-black dark canvas, add it as an extra name. Do not renumber 900. Steps in hundreds make it easy to add a half step later.

How the steps should be built

Even HSL stops look even in a panel and uneven in a UI. Yellow collapses. Blue spreads. Building the scale in OKLCH (or Lab) keeps neighboring steps looking like neighbors, because you move along lightness the way eyes work.

A good generator should:

  1. Keep the hue steady enough that 50 and 900 still belong to 500.
  2. Let intensity fall toward the ends, so 50 is not neon and 900 is not dull ink.
  3. Place 500 on the input color — or tell you the input is too light or too dark to sit at 500.

That last point matters. A very light brand color cannot be 500. It is 200 or 300. Build the rest around that fact. Do not pretend the wash is the base.

Naming is the feature

"A named 10-step palette" is the point. The names are how the palette enters a design system.

Use two layers.

Primitives — the raw scale:

color.blue.50
color.blue.100
...
color.blue.900

Roles — what the product calls them:

color.bg.brand-subtle          → blue.50
color.border.brand             → blue.200
color.action.primary           → blue.500
color.action.primary-hover     → blue.600
color.text.on-brand            → white or blue.50, after a contrast check

Primitives can grow. Roles should stay few. If every new screen needs a new role, the primitives are wrong or the roles were never written down.

Do not name a primitive by its job (color.button). The button will change. The step will not. This is the same discipline as naming type steps by role rather than by pixel value.

Check contrast before you export

A 10-step scale that cannot put readable text on its own wash is not finished.

Lock these pairs first:

  • 500 on 50 and on white
  • 600 or 700 on 50 (text on a tinted banner)
  • white on 500, 600, and 700
  • 900 on 50 and on white
  • 400 on white — this pair fails often, and teams use it for "muted" text

If 400 cannot pass 4.5:1 on the page background, it is not a text color. It is a border or icon color. Put that in the token note. Not every step is a text color.

Use a contrast checker on the real pairs. Generating a scale and checking a scale are two different jobs. Use both tools.

Neutrals are a second palette

Do not ask the brand hue to act like gray. A product also needs a 10-step neutral scale for surfaces, body text, hairlines, and disabled labels. Most pixels in an interface are neutrals.

If your grays should feel a little blue, say so and make a named cool-gray scale. Do not reuse blue-200 as a page background and blue-800 as body text unless you want a one-color product on purpose.

One brand scale. One neutral scale. Extra scales only when a status color or a chart series needs its own 10 steps. That is a system. A folder of unrelated hex values is not.

Dark mode is a mapping, not a new brand

If the steps are solid, dark mode is a remap:

  • Light bg.default → neutral.50
  • Dark bg.default → neutral.900 (or 950)
  • Light action.primary → brand.500
  • Dark action.primary → brand.400 or .500, whichever still passes on the dark surface

Do not invent a second brand color late at night because the button vanished. Pick another step from the same named scale and check it.

From one color to something you can ship

  1. Take the brand hex.
  2. Generate a named 10-step scale.
  3. Look at the row. If 50 or 900 leaves the family, adjust and generate again.
  4. Check the pairs above.
  5. Add a neutral scale.
  6. Map a short list of roles.
  7. Export once. Do not add new hex values that are "almost blue-400."

A palette generator that starts from one color and returns named steps makes step 2 fast. If all you need is a short run of tints and shades for one accent's states, that is a different job and a smaller tool. The judgment stays with you. The scale should not keep changing.

Tools mentioned