Patterns and Shapes for Colour-Independent Charts
Permalink to "Patterns and Shapes for Colour-Independent Charts"A multi-series chart that distinguishes series only by colour fails anyone who cannot tell those colours apart: around one in twelve men and one in two hundred women with colour-vision deficiency, anyone viewing a greyscale printout, anyone in Windows High Contrast mode where colours are replaced, and many low-vision users. SC 1.4.1 Use of Color makes this a Level A requirement. The fix is a second visual channel — shape, dash or pattern — so each series has an identity that does not depend on hue.
This page covers which channel suits which chart type, how to draw patterns that stay legible, and how to make them adapt to themes. It belongs to chart colour & contrast.
Spec reference
Permalink to "Spec reference"SC 1.4.1 Use of Color (Level A): colour is not used as the only visual means of conveying information, indicating an action, prompting a response, or distinguishing a visual element. A chart legend that maps red to “Retail” and blue to “Web” conveys series identity by colour alone unless something else also distinguishes them.
Techniques that satisfy it in charts:
- Marker shapes on points (circle, square, triangle, diamond).
- Dash patterns on lines (
stroke-dasharray). - Fill patterns on areas and bars (SVG
<pattern>with hatching, dots, crosshatch). - Direct labels naming each series on the chart itself.
- Data labels or an adjacent table, where the identity is in text.
SC 1.4.11 still applies to the marks themselves (3:1 against the background), and pattern lines inside fills are graphics too.
When patterns help — and when they clutter
Permalink to "When patterns help — and when they clutter"Use shapes and dashes by default: they are cheap, subtle and readable. Two to five series with distinct dashes or markers are easy to tell apart.
Use fill patterns for grouped and stacked bars and areas, keeping them sparse — widely spaced hatching or dots at low density — so the pattern reads as texture, not noise. Offer a “patterns on” setting if your designers object to them by default; but then remember that the setting must be discoverable, and the default must still meet SC 1.4.1 through another means (labels, a table).
Avoid patterns on pies with many slices; they become unreadable. Label slices directly, or use a bar chart, which is easier to read for everyone.
The misapplication to name is a colour-blind-safe palette used as the only distinction. Palettes like Okabe–Ito help many users, but they do not help greyscale printing, high contrast mode or monochromacy, and they do not satisfy SC 1.4.1 on their own.
Annotated code example
Permalink to "Annotated code example"// Build a hatch pattern for the Retail series inside the chart's <defs>
const NS = 'http://www.w3.org/2000/svg';
function hatchPattern(defs, id, token) {
const p = document.createElementNS(NS, 'pattern');
p.setAttribute('id', id);
p.setAttribute('width', '6'); p.setAttribute('height', '6');
p.setAttribute('patternUnits', 'userSpaceOnUse');
p.setAttribute('patternTransform', 'rotate(45)'); // diagonal hatching
const bg = document.createElementNS(NS, 'rect');
bg.setAttribute('width', '6'); bg.setAttribute('height', '6');
bg.setAttribute('fill', `var(${token}-bg)`); // theme token, not a hex
const line = document.createElementNS(NS, 'line');
line.setAttribute('x1', '0'); line.setAttribute('y1', '0');
line.setAttribute('x2', '0'); line.setAttribute('y2', '6');
line.setAttribute('stroke', `var(${token})`);
line.setAttribute('stroke-width', '2.5');
p.append(bg, line);
defs.append(p);
}
hatchPattern(chartDefs, 'hatch-retail', '--series-2');
// SC 1.4.1: Web bars solid, Retail bars hatched
retailBars.forEach((bar) => {
bar.setAttribute('fill', 'url(#hatch-retail)');
bar.setAttribute('stroke', 'var(--series-2)'); // SC 1.4.11: solid edge
bar.setAttribute('stroke-width', '1.5');
});
// Line charts: dash per series, repeated in the legend
const DASHES = ['', '6 4', '2 3', '10 3 2 3']; // solid, dashed, dotted, dash-dot
const MARKERS = ['circle', 'square', 'triangle', 'diamond'];
series.forEach((s, i) => {
s.path.setAttribute('stroke-dasharray', DASHES[i % DASHES.length]);
s.markerShape = MARKERS[i % MARKERS.length]; // SC 1.4.1: shape per series
legendItem(s).querySelector('svg line').setAttribute('stroke-dasharray', DASHES[i % DASHES.length]);
});
Patterns use theme tokens rather than hard-coded colours, so in dark mode the hatch lines and background switch with the rest of the chart. The stroke on the patterned bar gives it an edge that meets 3:1 against the plot background even where the hatching is sparse.
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"Patterns and shapes are visual; the equivalent for screen reader users is text. Each series needs its name in the chart’s accessible structure, and each data point’s announcement should include the series.
| Encoding | Visual users | Screen reader users |
|---|---|---|
| Dash / shape / pattern | Distinguish series without colour | Not perceived — provide series names in text |
| Legend | Maps encoding to series name | Legend must be text, not a canvas drawing |
| Point announcement | Tooltip shows series | “Retail, Q2: 90 orders” |
| Data table | — | Series as column headers |
Integration context
Permalink to "Integration context"The contrast floor for every mark is in non-text contrast for chart series. Where patterns would clutter, direct labels are the alternative second channel — direct labelling instead of legends. In forced-colours mode, patterns and dashes are often all that is left to distinguish series; how to keep them visible there is in forced colours mode for SVG charts.
Chart.js supports pointStyle and borderDash natively, as shown in accessible Chart.js configuration.
Gotchas
Permalink to "Gotchas"Pattern scale at zoom. Patterns defined in userSpaceOnUse scale with the SVG; at small chart sizes they can become moiré. Test at your smallest rendered size.
Too many dash styles. More than four or five dash patterns become hard to tell apart. Beyond that, direct labels are clearer.
Canvas charts. Patterns in canvas need createPattern with an offscreen canvas; regenerate them on theme change.
Design system notes
Permalink to "Design system notes"Ship series encodings as tokens alongside series colours: --series-n, --series-n-dash, --series-n-marker, --series-n-pattern. Chart components apply all of them by default, so colour-only charts are impossible unless a team actively removes the second channel.
Testing checklist
Permalink to "Testing checklist"FAQ
Permalink to "FAQ"Is a colour-blind-friendly palette enough to meet SC 1.4.1?
No. It helps many users, but SC 1.4.1 requires that colour is not the only means of distinguishing information. Add shapes, dash patterns, fill patterns or direct labels.
Which colour-independent encoding should I use for line charts?
Dash patterns on the lines, marker shapes on the points, or direct labels at the end of each line. Repeat the same encoding in the legend.
Do patterns make bar charts harder to read?
Dense patterns can. Keep hatching sparse and high-contrast, give patterned bars a solid edge, and offer direct labels where patterns would crowd the chart.
How do screen reader users tell series apart?
Through text: series names in the legend, in each data point’s announcement, and as column headers in the chart’s data table.
Related
Permalink to "Related"- Non-text contrast for chart series — the contrast floor
- Direct labelling — the other colour-free encoding
- Forced colours mode — where only patterns survive