FSD Frontend System Design

Part IV β€” The Reach Β· Lesson 8.5

Color Contrast

In one line4.5:1 for body text, 3:1 for large text and UI controls β€” and colour must never be the only signal.

Where you've seen itThe elegant #aaa helper text under a form field, invisible on a phone outdoors.

  • Time13 min
  • DiagramContrast pass/fail swatches + colour-blind failure
  • Chapter8 Β· Accessibility

Diagram

diagram
THE THRESHOLDS (WCAG AA)

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ body text (< 18.66px)          4.5 : 1     ← the common oneβ”‚
  β”‚ large text (β‰₯ 18.66px bold,                                β”‚
  β”‚             or β‰₯ 24px)          3 : 1                      β”‚
  β”‚ UI components, focus rings,                                β”‚
  β”‚ icons carrying meaning          3 : 1                      β”‚
  β”‚ disabled controls               exempt (but be kind)       β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

  on white background:
    #767676  4.54:1  βœ“ pass   ← the lightest grey that passes
    #999999  2.85:1  βœ— fail   ← the one designers reach for
    #aaaaaa  2.32:1  βœ— fail
diagram
COLOUR ALONE IS NOT A SIGNAL

  βœ— red border = error, green border = valid
      ~8% of men can't reliably distinguish them

  βœ“ red border + ⚠ icon + "Card number is incomplete"
      three signals: colour, shape, text

  Same rule for charts: colour + pattern/label + direct annotation.
diagram
DARK MODE FLIPS THE PROBLEM

  light theme   dark text on light bg   β†’ watch text too LIGHT
  dark theme    light text on dark bg   β†’ watch text too DIM
                                        + pure white on pure black
                                          causes halation; use
                                          #e8eaf0 on #0d0f14, not
                                          #ffffff on #000000

  Both themes must be checked. Passing one proves nothing about
  the other.

How it works

  1. Check contrast at design time with the DevTools colour picker or a contrast plugin β€” retrofitting a palette late is expensive.
  2. Encode state with more than colour: an icon, text, a pattern, or a position change.
  3. Test both themes. A palette that passes in light mode routinely fails in dark (see the theme variables in this site's own CSS).
  4. Don't forget focus rings. They need 3:1 against the adjacent background, which fails often on coloured buttons.
  5. Check text over images β€” a hero caption needs a scrim or overlay to hold a stable ratio.

Trade-offs

RequirementPractical effect
4.5:1 body text#767676 is your lightest grey on white
3:1 UI componentsThin light borders usually fail
Not colour aloneEvery status needs an icon or label
Brand coloursOften need a darker variant for text use

Brand pushback is normal. The usual resolution: keep the brand colour for large display type and backgrounds, and add an accessible darker shade for body text and controls.

Interview angle

"Design gives you a palette that fails contrast. What do you do?"

  • Bring numbers, not opinions: show the measured ratio and which text sizes fail.
  • Propose a derived accessible shade for text and controls, keeping the brand colour for large type and fills.
  • Add the check to CI or design tooling so the next palette doesn't repeat it.

Recap

  • 4.5:1 body, 3:1 large text and UI, both themes checked.
  • Colour is never the only carrier of meaning.
  • Focus rings and text-over-image are the two most commonly missed cases.