Shin

One red, ten stops.

A design system colour ramp, before and after. Six reds that all started at the fill end became ten stops, each read against the two on-colours the app uses: white and #1B1B1B.

01 / Problem statement

A palette with badges is not a ramp

I lead the design system for a trading app. Investors read these colours every day. Designers and developers build from them.

Version 2.0 put contrast badges on every swatch and marked no answer. A designer picking a red for a button, a banner or a dark screen had to guess which stop was safe. The ramp also stopped at six stops that all started at the fill end.

Surface 2 is too light. It jumps about 50% from Surface 1, and the containers look washed out.
One red and one green fail on the secondary surface, and the negative button's label is hard to see.
Muted red and green look softer, but traders no longer read them instantly.
We need more shades per colour for contrast and hierarchy.
Colours are missing from the mapping, tokens share one hex, and some screens hard-code colours.
Switching between dark and light needs extra variables.
02 / Constraints

Four rules the ramp had to meet

  1. Two pages. Every colour has to read on white and on #1B1B1B.
  2. One fill for both modes. A fill with a white label needs 4.5:1 on the label and 3:1 against each page. That pins its relative luminance to 0.133 to 0.183.
  3. Brand colours are fixed. Blue #0043CE and orange #F78F20 were already set, so Error had to fit around them.
  4. Steps you can tell apart. Under 1.2:1 between neighbours and two stops read as one.
03 / Audit

Six stops don't reach

Top row: v2.0 Red. Bottom row: v2.1 Error, at the same width, so the missing stops show as empty space. v2.0 opens at #E96E6E, which is 3.03 on white, a fill. v2.1 opens at a tint that holds dark text at 14.9.

v2.0 Red row of six swatches stacked above the v2.1 Error row of ten swatches

A ramp is a stepper. Lightness per stop, v2.0 against v2.1:

Stepper chart of lightness per stop for the Error ramp, v2.0 against v2.1
v2.0 Redv2.1 Error
Stops610
Lightness range0.310.61
Huedrifts 22 to 27held at 30
Step between stops1.17 to 1.441.18 to 1.45
Passes both pages300 and 400, unmarked500, marked
04 / Decisions

What I chose, and what I gave up

The Trade screen in dark mode, before and after v2.1, with notes on what changed

The same trade ticket, before and after v2.1. Before: the current app colours, applied screen by screen. After: the v2.1 palette, applied by rule.

  1. Brighter fill over safer contrast. I solved two reds for the one-fill rule. One traded a little page contrast for a brighter, more brand-like colour. The other did the reverse. I took the brighter one.
  2. Cut the lookalikes. Four reds sat 1.06 to 1.12:1 apart and looked the same. I kept one, dropped two as duplicates, and derived two fillers on the ramp's own hue and chroma.
  3. Add a surface tint. v2.0 had no stop light enough to sit behind text. v2.1 starts at one.
  4. Hold the hue. Dark reds stay red. They don't drift toward brown, which is what happened to the amber ramp.
  5. Script proposes, I decide. Values are derived in OKLCH by script, judged by eye, and placed on the board by hand. The script never shipped a stop. I did.

Neutrals step up by elevation. The brand fill sits only on the main action. Status colour appears only where it means something. Every text and icon colour passes WCAG on dark and on light. Light mode comes from the same tokens, so there is no second palette to maintain.

For Error, one fill, stop 500, serves both modes with a white label. The 46 colours are in code as tokens.

05 / Validation

Same rules, five real screens, dark mode

I applied the v2.1 palette by rule to five screens in both modes, ten in all. Dark mode is shown here, before on the left and after on the right. The Trade screen is under Decisions.

Dark mode before and after pairs for Home, Me, Order details and Market data agreement
  • Contrast recomputed from the hex for every Error stop. My Foundations file records every text and icon colour on these screens as passing WCAG, on dark and on light.
  • Greys step up by elevation. A card on a card of the same colour now moves one level.
  • Status colour appears only where it carries meaning. Orange stays on the main action.
06 / Reflection

What I'd fix, and a check for your own ramp

Two things on my own board don't meet my own rules. Error 600 carries a trophy with a 2.92 reading on #1B1B1B, which fails the 3:1 rule, so only 500 should have it. And the step from 50 to 100 is 1.18, a hair under my 1.2 line.

Five questions I'd now ask of any ramp:

  1. Do two stops look the same? The first cut of v2.1 had four reds 1.06 to 1.12:1 apart. The board now steps 1.18 to 1.45.
  2. Is the lightest stop a surface or a fill? v2.0 opens at a fill. v2.1 opens at a 50 that sits behind text.
  3. Does one stop take a white label in both modes? That needs 4.5 on the label and 3 against white and against the dark page. Mark it.
  4. Can you read contrast from the step numbers? The US Web Design System and Material Design both fix a gap between step numbers, so any two steps that far apart meet a contrast level: 40 apart for 3:1, 50 for 4.5:1, 70 for 7:1. A designer picks a pair by number and doesn't need a checker. Material treats it as a rule of thumb, so a real ratio is still worth checking.
  5. Do the red and the green stay apart without hue? Many colour-blind users can't tell a red from a green by hue alone. Giving the two different lightness, and adding a sign or icon, keeps profit and loss readable. WCAG asks the same: colour can't be the only signal.

The old row had the colour. Nobody had asked the questions.