Topic 10.2
Colour Vision and Contrast
In one line
About 1 in 12 men and 1 in 200 women see some colours differently, and everyone loses colour on a bright wall screen. Test a palette with a simulation and with contrast ratios, and never let colour alone carry a meaning.
Think of it like this
A map of train lines where "red line" and "green line" differ only in hue. For most travellers it is fine. For a traveller with red-green colour blindness both lines are the same muddy brown, and the map fails exactly when they need it. Printing the line's name next to it (or a different dash) fixes the map for everyone.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- colour-vision deficiency
- A difference in how the eye's colour cells work, so some colours look alike. It is often called colour blindness.
- protan, deutan, tritan
- The three types: missing or weak red (protan), green (deutan) or blue (tritan) cone cells.
- simulation
- Computing what a colour looks like to someone with a given type, so you can test a palette.
- contrast ratio
- The brighter colour's luminance plus 0.05, divided by the darker one's plus 0.05.
- redundant coding
- Saying the same thing with two cues, such as colour and shape, so losing one does not lose the meaning.
Step by step
01Simulate three kinds of vision
The first example runs three palettes through the protan, deutan and tritan matrices. In the picture, each row of six squares is a palette as seen by one kind of vision.
02Read the closest pair
The old Phase 7 palette's closest pair is the yellow and the orange (4 and 5): 22.4 apart for normal vision, 6.0 for deutan vision. That is the rider's report, found with code.
03Compute contrast, without rounding
The second example computes the ratio for text and for graphics. It shows that Westpark's yellow-orange and Hillcrest's light blue fail 3:1 on white, so the palette is changed to colours that pass.
04Add a second cue
The third example draws six lines with a symbol each and a name at each line end. Even if colours merge, the lines can still be told apart.
Try it yourself
- 1
Test your own colour
In the second example add
console.log(contrast("#767676", "#ffffff").toFixed(2)). It prints4.54: the lightest plain grey that passes 4.5:1 for text on white. A grey one step lighter (#777777) gives 4.48 and fails. - 2
Red and green, the classic pair
In the first example, add
"Red and green": ["#d62728", "#2ca02c"],as a fourth palette. Its lines readNormal ... distance 116.2,Protan 37.2,Deutan 7.2andTritan 126.2. Two colours that are far apart for most people fall under the rule-of-thumb 10 for deutan vision. - 3
Let the labels collide
In the third example change
ends[j - 1].label + 13toends[j - 1].label + 0. The label gap line printssmallest gap between labels: 1: two lines end one unit apart, so with 12-unit text their names would be drawn on top of each other. The 13 is what keeps them readable.
Code & diagrams
Edge case in the numbers: the Okabe-Ito palette and the Lumen palette stay above 15 apart for every kind of vision, but the Phase 7 palette's yellow and orange (4 and 5) fall to 6.0 for deutan vision. In the picture, the Phase 7 rows turn into greys and olives, with the last three squares almost the same. The simulation is a model, not a person: real vision varies, which is why severity 1.0 (the strongest case) is used.
Preview
Console
Nothing logged yet.Expected console output
Phase 7 palette Normal closest pair: 4 and 5 distance 22.4
Phase 7 palette Protan closest pair: 4 and 5 distance 11.8
Phase 7 palette Deutan closest pair: 4 and 5 distance 6.0
Phase 7 palette Tritan closest pair: 4 and 5 distance 20.4
Okabe-Ito six Normal closest pair: 1 and 5 distance 26.1
Okabe-Ito six Protan closest pair: 4 and 5 distance 20.6
Okabe-Ito six Deutan closest pair: 3 and 4 distance 18.0
Okabe-Ito six Tritan closest pair: 1 and 3 distance 15.8
Lumen six Normal closest pair: 1 and 6 distance 25.4
Lumen six Protan closest pair: 3 and 5 distance 19.8
Lumen six Deutan closest pair: 3 and 4 distance 18.0
Lumen six Tritan closest pair: 1 and 3 distance 15.8Failure cases found by computing: the light grey the dashboard used for the old gridlines and labels (#9aa8b4) is 2.43:1, so it fails even the graphics threshold. Hillcrest's light blue and Westpark's orange fail 3:1 on white (2.31 and 2.25) though both pass on dark. That is why the Lumen palette in this phase replaces those two colours with #F0375D and #0096FF, which pass on white (3.88 and 3.09) and on the dark card (4.17 and 5.24).
Console
(nothing was printed)Expected console output
black on white: 21.00
same colour twice: 1.00
Muted text #5b6b78 on white 5.50 text 4.5: pass | graphic 3: pass
Light grey #9aa8b4 on white 2.43 text 4.5: FAIL | graphic 3: FAIL
Ink #1d2b36 on white 14.47 text 4.5: pass | graphic 3: pass
--- the Okabe-Ito colours on white (graphics need 3:1) and on dark #16222b
Harbour #0072B2 on white 5.19 pass | on dark 3.12 pass
Old Town #D55E00 on white 3.87 pass | on dark 4.18 pass
University #009E73 on white 3.42 pass | on dark 4.73 pass
Riverside #CC79A7 on white 3.06 pass | on dark 5.29 pass
Hillcrest #56B4E9 on white 2.31 FAIL | on dark 7.01 pass
Westpark #E69F00 on white 2.25 FAIL | on dark 7.18 pass
grey that shows as 3.0 but fails: #959595 2.9953 FAILIn the picture: two high lines (Harbour with circles, Old Town with squares) cross twice around May and July and peak near 30k, University (triangles), Riverside (diamonds) and Hillcrest (crosses) run in a band between about 5k and 18k, and Westpark (stars) sits at zero until April, then rises. The labels at the right are in order and pushed 13 units apart when two lines end close together. Edge case seen: Westpark's four zero months draw a flat line on the axis, which is correct: those stations opened in May.
Preview
Console
Nothing logged yet.Expected console output
labels at the line ends: Harbour, Old Town, University, Riverside, Hillcrest, Westpark
smallest gap between labels: 13
pairs that greyscale cannot tell apart: 12 of 15
all six symbols differ: true | all six labelled: trueBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Round before you compare
In the second example change verdict to (ratio, need) => (Math.round(ratio * 10) / 10 >= need ? "pass" : "FAIL").
Myth vs fact
Myth
Colour blindness means seeing in black and white.
Fact
Almost everyone with a colour-vision deficiency sees colour. Some colour pairs merge, mostly red-green. Seeing no colour at all is very rare.
Myth
If the palette is colour-blind safe, I need no labels.
Fact
A safe palette still fails in a greyscale print, on a glared wall screen, or with a low-contrast monitor. Shapes and labels cost little and cover all of those.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The matrices apply to linear RGB, so the code decodes the sRGB values first and encodes the result. Applying them to gamma-encoded numbers gives a plausible-looking but wrong picture. The page that publishes the matrices does not say which space to use; common implementations decode to linear first, as this example does, so treat the output as a model and check it against the paper if you rely on it.
- ▸
WCAG's 3:1 for graphics applies to the parts needed to understand the chart, against the colours next to them. A bar needs 3:1 against the card behind it, and a legend swatch needs it too. WCAG's own text counts the lines in a graph, "including the background lines for the values", as graphical objects; it exempts a chart whose visible labels and values carry the same information. So do not rely on a gridline that is 1.3:1 against the card (this build's
--chart-grid) to be the only way to read a value: label the axis. Lines that rarely overlap must contrast with the background, not with each other.
Remember this
- 1
The problem this solves: a rider who is colour blind wrote that two districts in the stack look identical. Nobody on the team could see it, because nobody on the team shared that rider's vision. You cannot check a palette by eye. You check it with a simulation and with numbers.
- 2
What colour-vision deficiency is. The eye has three kinds of cone cells for colour. When one kind is missing or shifted, some colours merge. The National Eye Institute describes the red-green types (deuteranomaly, the most common, and protanomaly, both mild; deuteranopia and protanopia, where red and green cannot be told apart at all), the rarer blue-yellow types (tritanomaly and tritanopia), and the very rare case of seeing no colour. Colour Blind Awareness gives about 1 in 12 men (8%) and 1 in 200 women.
- 3
Simulate it with a matrix. Machado, Oliveira and Fernandes (2009) published 3×3 matrices that turn a colour into what a person with protan, deutan or tritan vision sees. You convert a colour to linear-light RGB, multiply, and convert back. The first example does that for three palettes and prints the closest pair: the two colours nearest each other in Lab distance, which is how far apart they look. The rule of thumb here is that a distance under about 10 is hard to tell apart.
- 4
Contrast is a ratio of brightness. WCAG 2.2 (the Web Content Accessibility Guidelines) defines relative luminance for a colour (a weighted mix of its linear red, green and blue) and a contrast ratio
(L1 + 0.05) / (L2 + 0.05)whereL1is the lighter colour. It runs from 1:1 (same colour) to 21:1 (black on white). Success criterion 1.4.3 asks for 4.5:1 for normal text (3:1 for large text, 18 pt or 14 pt bold), and 1.4.11 asks for 3:1 for the parts of a graphic needed to understand it (a bar, a line, a marker), at level AA. - 5
The threshold is strict. WCAG says the computed value must not be rounded (its own example: 4.499:1 does not meet 4.5:1), and the same holds for the 3:1 of 1.4.11, so 2.999:1 fails. The second example finds a grey,
#959595, that prints as3.0when rounded to one decimal and still fails with 2.9953. Compute the ratio, compare it with>=, and never round first. - 6
Never let colour be the only signal. Even a perfect palette fails in a greyscale print or on a glare-covered wall screen. Redundant coding adds a second cue: a symbol shape, a dash pattern, a direct label at the line end, a value printed on the bar. The third example puts all of those on the six-district chart, and measures that lightness alone cannot separate 12 of the 15 pairs of colours.
Explain it without notes
Why can't you check a palette for colour-blind readers by looking at it?
What do the WCAG ratios 4.5:1 and 3:1 apply to?
Why is rounding a contrast ratio before comparing it a mistake?
Practice
Write passes(fg, bg, kind) that returns true when the contrast of fg on bg meets 4.5 for "text" and 3 for "graphic". Test #767676 and #777777 on white as text.
In the Lumen palette, which pair is closest for tritan vision, and what would you do if the distance were under 10?
Trade-offs
- ↔
A palette tested for colour-blind safety has fewer choices of hue, so it may look less lively. The tested palette is the one that works for more people.
Done when you can
I can say roughly how common colour-vision deficiency is, and name its main types.
I can simulate a palette and read the closest pair.
I can compute a WCAG contrast ratio and compare it without rounding.
I can add a second cue so colour is never the only signal.