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"
Four layers of colour-safe charts Layers of a colour-accessible chart: contrast of marks, a second visual channel, direct labels, and theme and forced-colours adaptation. Four layers of colour-safe chartsContrastevery mark 3:1 against the plot background; labels 4.5:1Second channeldash, marker shape or fill pattern per seriesDirect labelsseries names beside the data instead of a colour keyAdaptationtokens per theme; forced-colours stylesheet with system colours
Each layer removes one way colour can fail; together they make colour a reinforcement rather than a requirement.

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; }
}
A chart palette review Flow of a chart palette review: token contrast check, second channel assignment, label placement, dark theme check and forced colours check. A chart palette reviewTokens3:1 per themeSecond channeldash, shape,patternLabelsdirect, 4.5:1Dark themere-checkForced coloursstill readable?
Run this once per palette and once per new chart type; CI covers the token checks thereafter.

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
Colour failure modes and their fixes Matrix of common chart colour failures, who is affected and the fix for each. Colour failure modes and their fixesFailureWho is affectedFixPastel series on whiteLow vision, projectorsTokens at 3:1Colour-only legendColour-vision deficiencyDashes, shapes, labelsRed/green statusDeuteranopia, protanopiaIcons and wordsHard-coded SVG coloursDark mode, forced coloursClasses and tokensbox-shadow focus ringForced coloursOutline or stroke
Every fix on this list is cheap once it lives in the chart component.

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.

Series distinguishable in greyscale, by encoding Bar chart of how many of five series remain distinguishable in a greyscale rendering for colour-only, colour plus dashes, and colour plus dashes plus direct labels. Series distinguishable in greyscale, by encodingColour only2 of 5 series — mid-tones merge in greyscaleColour + dashes5 of 5 seriesColour + dashes + labels5 of 5 series
An example five-series line chart rendered in greyscale — colour alone leaves two distinguishable series; adding dashes and labels makes all five distinct.

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:

  1. 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.
  2. Add dashes and markers. Most libraries support stroke-dasharray, borderDash or pointStyle per series. Turning these on is usually configuration only.
  3. 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.
  4. Add direct labels to line charts. Start with the charts that have the most series.
  5. Add the forced-colours stylesheet. Map chart roles to system colours and check with emulation.
  6. 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.

Permalink to "Related"

← Back to Virtualization, Charts & Dynamic Data Displays