Skip to content

Color System

The palette plus the rules for applying it — semantic roles, contrast requirements, and the values behind each mode.

Deliverables & Artifacts

A color system is the palette plus the rules for applying it. The palette alone is a set of values that every team member interprets differently.

The difference shows up on the first rebrand or the first dark mode, when a system takes a day and a palette takes a sprint of searching for hex codes.

Semantic roles carry the system

A component should ask for surface-raised, border-subtle, text-primary, action-danger — not gray-100 and certainly not #F4F4F5.

Two layers:

Primitives. The raw values. blue-500, gray-900. No meaning attached.

Semantics. The roles that point at primitives. action-primary: blue-500. text-body: gray-900.

Components reference only the second layer. That indirection is what makes dark mode a change to one mapping file rather than a change to two hundred components. See design tokens.

Shades

Nine to eleven steps per hue is the usual working range. It covers backgrounds, borders, fills, hover states, and text without improvisation, and stops short of the point where adjacent steps become indistinguishable.

Generate perceptually, not mathematically. Evenly spaced lightness values in HSL are not evenly spaced to the eye — yellows appear to jump, blues appear to stall. Tools working in a perceptual space handle this; picking by eye in HSL does not.

Contrast is measured against the real background

WCAG AA asks for 4.5:1 on body text and 3:1 on large text, and 3:1 on interface components and their states.

The failure mode is not ignorance of the rule. It is testing against white when the component sits on a tinted surface. A gray that passes on #FFFFFF can fail on #FAFAF9, and the component that renders inside a card is the one nobody checked.

Put the requirement in the token layer: if text-secondary is only permitted on surface-default and surface-raised, that is a documentable fact, not something rediscovered per screen.

Dark mode

Not an inversion.

Surfaces get lighter with elevation, where light mode uses shadow. Inverting a light theme removes the elevation language entirely.

Pure white on pure black is harsh. Most dark themes use an off-white around 87% opacity on a near-black rather than true extremes.

Saturated colours need adjusting. A brand blue that reads well on white often vibrates on a dark surface and needs a lighter, less saturated variant.

All of this is expressible as a second mapping of semantics to primitives, which is the argument for the two-layer structure.

Never colour alone

WCAG 1.4.1 requires a second cue, and around one in twelve men has some form of colour vision deficiency.

A red border on a failed field needs an icon or a message. A status chart needs labels as well as hues. A required field marked only by a red asterisk marked only by colour is two failures in one convention.

In practice

A product ships dark mode by inverting lightness values across the palette.

The result is legible and flat. Cards, modals, and dropdowns all land on similar surfaces, because the light theme separated them with shadow and shadow does nothing on black. Customers describe it as "hard to follow" without being able to say why.

The repair is a separate dark mapping where each elevation level gets its own surface value, lighter as it rises. Same components, same semantic names, different values behind them.

The inversion was faster to ship and it produced a theme that had to be rebuilt.

Where teams get it wrong

  • Hex codes in components. A rebrand as a search-and-replace.
  • No semantic layer. Dark mode touching every component.
  • Contrast checked against white. Failures inside tinted cards.
  • Inverted dark mode. Elevation language lost.
  • Even lightness steps in HSL. Shades that jump and stall.
  • Colour as the only signal. A state invisible to some readers.

Frequently Asked Questions