Chart Colour & Contrast
Permalink to "Chart Colour & Contrast"Colour is the first tool chart designers reach for and the first thing that fails for users. A palette that distinguishes six series beautifully on a designer’s monitor can be six shades of the same brown to a user with deuteranopia, a single colour in Windows High Contrast mode, and invisible pastel lines on a projector. WCAG addresses this through two criteria that apply directly to charts: SC 1.4.1 Use of Color (colour cannot be the only way information is conveyed) and SC 1.4.11 Non-text Contrast (the marks users need must reach 3:1 against what surrounds them).
This topic turns those criteria into chart practice: contrast for lines, bars and focus rings; shapes, dashes and patterns as a second channel; direct labels in place of colour-keyed legends; and charts that survive dark mode and forced colours. It is written for engineers building chart components and for designers and design system maintainers who own chart palettes.
It belongs to virtualization, charts & dynamic data displays.
WCAG criteria in scope
Permalink to "WCAG criteria in scope"| Criterion | Level | Relevance to chart colour |
|---|---|---|
| 1.4.1 Use of Color | A | Series, states and categories need a non-colour distinction |
| 1.4.11 Non-text Contrast | AA | Data marks and focus indicators reach 3:1 against adjacent colours |
| 1.4.3 Contrast (Minimum) | AA | Labels, axes and legends reach 4.5:1 (3:1 if large) |
| 1.3.3 Sensory Characteristics | A | Instructions do not rely on colour alone (“the green line”) |
| 2.4.7 / 2.4.11 Focus Visible / Not Obscured | AA | Focus on data points is visible, including in forced colours |
Prerequisites
Permalink to "Prerequisites"- A chart with a text alternative and data table — see data visualization & chart alternatives — because no colour technique replaces text for screen reader users.
- Theme tokens and a dark theme; charts should consume tokens rather than library defaults.
- The contrast ratio formula (relative luminance), used throughout, as in auditing focus indicator contrast for WCAG 2.4.11.
ARIA & HTML spec reference
Permalink to "ARIA & HTML spec reference"Colour is a visual concern, so most of this topic’s reference is CSS and SVG rather than ARIA.
| Feature | Values | When to apply | Common misuse |
|---|---|---|---|
stroke-dasharray |
dash lengths | Line series identity | Only one dashed series among many solid |
SVG <pattern> |
fill pattern | Bars and areas | Dense patterns that create moiré |
@media (forced-colors: active) |
— | Map chart roles to system colours | Absent; charts vanish in high contrast |
forced-color-adjust |
auto, none |
Opt colour-scale marks out, with borders | Whole-chart opt-out |
| System colours | Canvas, CanvasText, Highlight, … |
Forced-colours styling | Hard-coded hex in the media query |
role="img" + description |
— | Name series in text for AT | Relying on colour words in the description |
Step-by-step implementation
Permalink to "Step-by-step implementation"Step 1 — Check mark contrast per theme (SC 1.4.11)
Permalink to "Step 1 — Check mark contrast per theme (SC 1.4.11)"for (const t of themes) for (const c of t.series)
if (ratio(c, t.plotBg) < 3) fail(`${t.name} ${c}`);
Step 2 — Add a second channel per series (SC 1.4.1)
Permalink to "Step 2 — Add a second channel per series (SC 1.4.1)"series.forEach((s, i) => s.path.setAttribute('stroke-dasharray', DASHES[i]));
Step 3 — Label directly (SC 1.4.1, 1.4.3)
Permalink to "Step 3 — Label directly (SC 1.4.1, 1.4.3)"renderLabels(g, placeEndLabels(series, x, y)); // names beside line ends
Step 4 — Style through tokens and classes (SC 1.4.11 in every theme)
Permalink to "Step 4 — Style through tokens and classes (SC 1.4.11 in every theme)".series-1 { stroke: var(--series-1); }
[data-theme="dark"] { --series-1: #6ddfa0; }
Step 5 — Support forced colours (SC 1.4.1, 2.4.7)
Permalink to "Step 5 — Support forced colours (SC 1.4.1, 2.4.7)"@media (forced-colors: active) {
.series-line { stroke: CanvasText; }
.point.is-active { stroke: Highlight; }
}
Keyboard interaction contract
Permalink to "Keyboard interaction contract"Colour decisions surface in keyboard use through focus and selection states on data points.
| Key | Context | Action | Visual requirement | Failure indicator |
|---|---|---|---|---|
Tab |
Chart | Focus the chart | Outline 3:1, visible in forced colours | box-shadow ring vanishes in high contrast |
| Arrow keys | Chart | Move active point | Ring 3:1 against point and background | Ring in the series colour |
Space |
Selectable chart | Select point | Selection shown by shape or outline, not tint alone | Lighter tint only |
Escape |
Tooltip open | Dismiss | Tooltip border 3:1 | Borderless tooltip merges with the plot |
Screen reader compatibility matrix
Permalink to "Screen reader compatibility matrix"Screen readers do not perceive colour; the matrix shows what must be in text instead.
| AT | What colour conveys visually | Text equivalent required |
|---|---|---|
| NVDA / JAWS | Series identity | Series names in point announcements and table headers |
| VoiceOver | Highlighted or selected state | “selected” state via ARIA, not only a fill change |
| TalkBack | Threshold colouring (red = over budget) | Words: “over budget” in labels and table |
| All | Heat map intensity | Values in the table; scale described in text |
Edge cases & failure modes
Permalink to "Edge cases & failure modes"1. Dark mode reuses the light palette
Permalink to "1. Dark mode reuses the light palette"Diagnosis: mid-tone series colours pass 3:1 on white and fail on near-black. Fix: define series tokens per theme and check each.
2. Hover dimming drops series below 3:1
Permalink to "2. Hover dimming drops series below 3:1"Diagnosis: hovering one series fades the others to 20% opacity. Fix: dim less, or emphasise the hovered series with thickness instead.
3. Status colours without words
Permalink to "3. Status colours without words"Diagnosis: red and green cells in a table or bars in a chart mean over and under target. Fix: add icons and text (“Over by 12%”).
4. Patterns that vanish at small sizes
Permalink to "4. Patterns that vanish at small sizes"Diagnosis: hatching becomes grey mush in sparklines. Fix: use shape or label encodings at small sizes; patterns only where there is room.
5. Charts as images
Permalink to "5. Charts as images"Diagnosis: PNG exports of charts cannot adapt to themes or forced colours. Fix: inline SVG with token-driven styles, plus a data table.
Cross-cutting concerns
Permalink to "Cross-cutting concerns"Palettes are systems, not lists. A chart palette is usually defined as a list of hex values in a library config. For accessibility it needs to be a system: each series defined by a mark colour, a text colour, a dash, a marker shape and a pattern, per theme, with its contrast ratios recorded. That is more work up front and far less work afterwards, because every chart built from the system inherits every guarantee.
Semantic colours need semantic words. Status colours — green for on target, amber for at risk, red for over budget — are the most common colour-only encoding in dashboards and tables. The fix is not a better red; it is the word “Over budget” next to the value, and an icon that differs in shape. Colour then reinforces a meaning already present in text.
Order of series. Series order affects which colours sit next to each other in stacked and grouped charts, and which series get the most distinguishable encodings. Put the series users care most about first, so it gets the strongest colour, the solid line and the simplest shape.
Printing and exports. Charts printed in greyscale or exported into presentations lose colour and context. Direct labels and dashes survive printing; hover tooltips and interactive legends do not. If users regularly export charts, design for the static, greyscale case.
Brand constraints. Brand palettes often include colours that cannot reach 3:1 on white — pale yellows, light greys, mint greens. Use them for large backgrounds and decorative elements, and keep data marks to the brand colours that pass, or to darker variants derived from them.
Animation and emphasis. Emphasis by colour change — highlighting a series on hover by making the others grey — is a colour-only state change. Pair it with thickness, a label or an outline, and make the same emphasis available from the keyboard when a series or point is focused.
Non-text contrast for chart series
Permalink to "Non-text contrast for chart series"Non-text contrast for chart series sets out which chart parts need 3:1, how to check series tokens in both themes with the luminance formula, and why a background-coloured gap beats hunting for neighbour-safe palettes in stacked charts.
.stack-segment { stroke: var(--plot-bg); stroke-width: 2px; }
Behaviour note: 3:1 makes each series visible; it does not make series distinguishable from each other.
Patterns and shapes
Permalink to "Patterns and shapes"Patterns and shapes for colour-independent charts matches second channels to chart types — dashes for lines, markers for points, fills for bars — and builds token-driven SVG patterns.
bar.setAttribute('fill', 'url(#hatch-retail)');
Behaviour note: repeat the same encoding in the legend, or the legend is still colour-only.
Direct labelling
Permalink to "Direct labelling"Direct labelling instead of legends places series names beside their data, resolves collisions, and uses darker text tokens so labels reach 4.5:1 while keeping the series hue.
g.selectAll('text.series-label').data(labels).join('text');
Behaviour note: labels are visual; screen readers get series names from the description and table.
Forced colours mode
Permalink to "Forced colours mode"Forced colours mode for SVG charts shows what high contrast themes do to charts, maps chart roles to system colours, and limits forced-color-adjust: none to colour-scale marks with outlines.
@media (forced-colors: active) { .chart:focus-visible { outline: 2px solid Highlight; } }
Behaviour note: forced colours is the proof that a chart’s second channel works.
Who these decisions affect
Permalink to "Who these decisions affect"Users with colour-vision deficiency — most commonly red–green, affecting roughly one in twelve men — lose any distinction carried by hue alone. A second channel is the fix; a “colour-blind-safe” palette helps but is not enough.
Low-vision users need marks that stand out from the background and labels large and contrasted enough to read at zoom. Direct labels help most at high zoom, where a legend may be off-screen.
Users of forced colours, often low-vision or light-sensitive, see charts through their own palette. Charts built from classes and tokens with a forced-colours stylesheet remain readable; charts built from hard-coded colours may vanish.
Screen reader users perceive none of this directly. For them, the equivalents are text: series names, state words, and values in a table. Colour work and text alternatives are complementary, not interchangeable.
Where to start with an existing chart library
Permalink to "Where to start with an existing chart library"Most teams inherit a chart library with its own default palette. A workable order for bringing it up to standard:
- Measure the defaults. Run the luminance check on the library’s default series colours against your plot backgrounds in both themes. Replace any that fall below 3:1 with tokens.
- Add dashes and markers. Most libraries support
stroke-dasharray,borderDashorpointStyleper series. Turning these on is usually configuration only. - Replace canvas legends with HTML legends. HTML legends can show the same dash and marker, are readable by screen readers, and can be keyboard-operable toggles.
- Add direct labels to line charts. Start with the charts that have the most series.
- Add the forced-colours stylesheet. Map chart roles to system colours and check with emulation.
- Replace status colours with words and icons wherever colour signals good or bad.
Steps one and two cover most audit findings about chart colour; the remaining steps make charts robust in the environments — print, high contrast, greyscale — where colour disappears entirely.
Design system integration
Permalink to "Design system integration"| Token or component | Requirement | Criterion |
|---|---|---|
--series-n per theme |
3:1 against the plot background, verified in CI | 1.4.11 |
--series-n-text per theme |
4.5:1 for labels in the series hue | 1.4.3 |
--series-n-dash, -marker, -pattern |
A second channel for every series | 1.4.1 |
--plot-bg as separator |
Gaps between stacked segments | 1.4.11 |
| Chart forced-colours stylesheet | Roles mapped to system colours | 1.4.1, 2.4.7 |
| Status tokens | Always paired with icon and text | 1.4.1 |
Testing checklist
Permalink to "Testing checklist"Automated
Permalink to "Automated"Visual
Permalink to "Visual"Screen reader
Permalink to "Screen reader"FAQ
Permalink to "FAQ"What contrast do chart lines and bars need?
At least 3:1 against the colours next to them, usually the plot background, under SC 1.4.11. Text in charts — labels, axes, legends — needs 4.5:1, or 3:1 if it is large.
Is a colour-blind-safe palette enough for accessible charts?
No. SC 1.4.1 requires that colour is not the only way to tell series or states apart. Add dashes, marker shapes, fill patterns or direct labels as a second channel.
How many colours can a chart palette safely use?
Most charts stay readable with up to five or six series if each also has a distinct dash, shape or pattern. Beyond that, even good encodings become hard to tell apart; consider small multiples, grouping minor series into “Other”, or direct labels.
Are red and green status colours always a problem?
They are a problem when they are the only signal, because red–green colour-vision deficiency is the most common kind. Pair them with words and differently shaped icons, and the colours become a helpful reinforcement.
Do charts need to support Windows High Contrast mode?
They should remain readable in it. Style charts with classes and tokens, distinguish series with dashes or patterns, and add a forced-colours stylesheet that uses system colours for text, lines and focus.
Related
Permalink to "Related"- Chart alternatives — text and table equivalents
- Chart keyboard interaction — focus rings on data points
- Data dashboards — many charts on one page
- Focus indicator contrast auditing — measuring contrast in practice
- Manual audits — forced colours and zoom checks