CodeChroma All articles
Accessibility & Colour Systems

The Legend Is the Interface: A Cartographic Colour System for Accessible Web Controls

CodeChroma
The Legend Is the Interface: A Cartographic Colour System for Accessible Web Controls

Photo: Adrian Dust , CC BY-SA 2.0, via Wikimedia Commons

A folded OS Explorer map is a masterclass in restrained colour. Contour lines are brown, woodland is green, access land is washed red, and water is a blue that never shouts. The legend is not decoration; it is the interface. If you can read the key at arm's length in the rain, the colour system has done its job.

Web interfaces could learn from that. Not by copying the exact inks — print and screens have different constraints — but by borrowing the relationships. Map colours are semantic, repeated, and tested against a busy background. That is close to what a design token set should be.

Start with the legend, not the hero

I begin by listing the features an interface actually needs to distinguish: primary text, secondary text, links, controls, disabled states, warnings, success, water or utility, land or neutral, and a boundary. Then I match each one to a cartographic idea. The map legend gives me a vocabulary; the product gives me the priorities. A public-sector service might need more contrast on links than a portfolio site, but the map logic still holds.

Sample the legend, but do not sample pixel-for-pixel. Pick the hue family and the relative lightness. Contour brown is warm and mid-dark; woodland green is cooler and slightly deeper; access red is a signal colour with high chroma; water blue is bright enough to separate from paper but not neon. Translate those relationships into OKLCH so you can adjust lightness and chroma without hue drift.

A small OKLCH token set

Here is a starting set. The values are deliberately restrained: enough chroma to read as map colours, not so much that they fight each other. Use them as a base, then tune against your own surface colour.

:root {
  --map-paper: oklch(97% 0.012 85);
  --map-ink: oklch(24% 0.03 80);
  --map-contour: oklch(62% 0.08 75);
  --map-woodland: oklch(57% 0.09 145);
  --map-water: oklch(64% 0.11 235);
  --map-access: oklch(55% 0.17 28);
  --map-path: oklch(58% 0.10 55);
  --map-muted: oklch(48% 0.02 85);
}

The raw hues are for symbols, fills and borders. For text, do not use the same lightness. A 64% water blue on a 97% paper background may look fine as a lake, but it will fail as link text. Create darker text variants by dropping lightness while keeping hue and roughly similar chroma:

:root {
  --map-water-text: oklch(38% 0.11 235);
  --map-woodland-text: oklch(38% 0.09 145);
  --map-access-text: oklch(42% 0.17 28);
}

This is the crucial move. Map legends use colour on small marks and large fills; web interfaces use colour on 16px text and 1px borders. The token names should reflect the job, not just the hue. --map-water is a fill. --map-water-text is a text-safe variant. If you only keep one, keep the text-safe one and use opacity or a lighter surface for fills.

Check the contrast, then check it again

OKLCH makes it easy to explore lightness and chroma, but it does not tell you whether a pair passes. Run the numbers. For body text, aim for at least 4.5:1 against the background. For large text and UI boundaries, 3:1 is the usual floor. For focus rings, aim higher: a focus indicator needs to be visible against both the control and the page, not just one of them.

A practical workflow: put your tokens into a small contrast checker, test every text and background pair you actually ship, and record the results in the repo. Do not rely on a single 'brand' pair. Test the hover state, the disabled state, the error state and the dark-mode inversion. Map colours often sit on cream paper; on a dark surface, the same hue needs a different lightness. Keep the hue, change the L.

Build a legend that scales

The legend component is a good test bed because it combines colour, text and small shapes. Here is a compact inline SVG that uses the tokens. It has a title, a description, and a list of swatches with labels. The text uses currentColor so it inherits the page's accessible text colour; the swatches keep their semantic fills.

<svg class='map-legend' viewBox='0 0 320 180' role='img' aria-labelledby='legend-title legend-desc'>
  <title id='legend-title'>Map legend</title>
  <desc id='legend-desc'>Four sample map features: contour, woodland, water and access land.</desc>
  <g font-family='system-ui, sans-serif' font-size='13'>
    <rect x='0' y='0' width='320' height='180' fill='var(--map-paper)' rx='8'/>
    <g transform='translate(16 24)'>
      <path d='M0 6 Q20 -4 40 6 T80 6' fill='none' stroke='var(--map-contour)' stroke-width='2'/>
      <text x='92' y='10' fill='currentColor'>Contour</text>
    </g>
    <g transform='translate(16 64)'>
      <circle cx='8' cy='6' r='6' fill='var(--map-woodland)'/>
      <text x='92' y='10' fill='currentColor'>Woodland</text>
    </g>
    <g transform='translate(16 104)'>
      <rect x='0' y='0' width='18' height='12' fill='var(--map-water)' rx='2'/>
      <text x='92' y='10' fill='currentColor'>Water</text>
    </g>
    <g transform='translate(16 144)'>
      <rect x='0' y='0' width='18' height='12' fill='var(--map-access)' rx='2'/>
      <text x='92' y='10' fill='currentColor'>Access land</text>
    </g>
  </g>
</svg>

Two details matter. First, the SVG uses currentColor for labels, so the legend never invents a new text colour. Second, the swatches are simple blocks and strokes with a minimum stroke width. At small sizes, a 1px contour line disappears. Use 1.5px or 2px, and give the legend enough padding. If you need to shrink further, drop features rather than making every mark thinner.

For responsive sizing, set the SVG width to 100% and cap it with max-width. Use font-size: clamp(11px, 2.5vw, 13px) on the text group if you want it to breathe on mobile. But test at 200% zoom. A legend that only works at one size is a poster, not an interface.

The lesson from the paper map

The best map legends are boring in the right way. They repeat the same few colours, use shape as well as hue, and never ask you to decode a gradient in the rain. That is a good brief for a web palette, too. Borrow the cartographic relationships, convert them to OKLCH so you can tune them, and keep a text-safe variant for every signal colour. The result will not look like an OS sheet — and it should not. It will feel like a system that knows where it is.

All Articles

Related Articles

Flicking the Switch: A Tabletop-Football Palette That Survives Tiny Screens

Flicking the Switch: A Tabletop-Football Palette That Survives Tiny Screens

Mooring Lines and Contrast Ratios: A Canal-Boat Palette for the Web

Mooring Lines and Contrast Ratios: A Canal-Boat Palette for the Web

Galvanised, Gaffer and Grit: A VS Code Theme Sampled from the British Workshop

Galvanised, Gaffer and Grit: A VS Code Theme Sampled from the British Workshop