Direct Labelling Instead of Legends

Permalink to "Direct Labelling Instead of Legends"

A legend asks readers to look at a colour swatch, remember the colour, find the matching line, and repeat for every series. That is a memory task that colour-blind users often cannot complete at all, and that low-vision users at high zoom struggle with because the legend and the line are far apart on screen. Direct labelling removes the task: each series is named in text right next to its data — at the end of a line, above a bar group, inside a large segment.

This page covers placement rules, collision handling, contrast, and how direct labels relate to the chart’s text alternative. It belongs to chart colour & contrast.

Spec reference

Permalink to "Spec reference"

Direct labels satisfy SC 1.4.1 Use of Color by carrying series identity in text rather than in colour. As text, they fall under SC 1.4.3 Contrast (Minimum) — 4.5:1 for normal text, 3:1 for large (18.66px bold or 24px regular) — and SC 1.4.4 Resize Text.

In an inline SVG, labels are <text> elements, which screen readers can read if the SVG is not treated as a single image. For a chart exposed as role="img" with a name and description, the labels are presentational to assistive technology, and the series names belong in the description or data table instead. For a chart exposed as a structured graphic (for example, a role="group" with focusable points), each point’s name should include its series.

Related: SC 1.3.3 Sensory Characteristics — instructions should not rely on colour or position alone (“the green line”, “the series on the right”).

Legend lookup versus direct labels Comparison of reading a chart with a separate colour legend against reading one with series names placed directly beside each line. Legend lookup versus direct labels✗ Colour-keyed legendMatch swatch colour to line colourHard or impossible with colour-visiondeficiencyLegend far from data at high zoomHover to find which line is which✓ Direct labelsName sits at the end of each lineReadable in greyscale and forced coloursLabel travels with the data when zoomedNo lookup, no memory load
Direct labels turn a colour-matching task into reading.

When to label directly — and when a legend is still needed

Permalink to "When to label directly — and when a legend is still needed"

Label directly whenever there is room: line charts with up to about six series, grouped bar charts, stacked bars with large segments, slope charts. It is almost always clearer.

Keep a legend (or add a text key) when labels would overlap unavoidably — ten tangled lines, small multiples sharing a key, very narrow charts — and pair it with shapes or dashes so the legend is not colour-only. A legend rendered as HTML text next to the chart is also better for screen readers than one drawn inside a canvas.

The misapplication to name is direct labels that are themselves colour-only — tiny coloured dots at line ends with the name in a tooltip. The text must be visible.

Annotated code example

Permalink to "Annotated code example"
// D3-style direct labels at line ends, with simple collision avoidance
const LABEL_GAP = 16;                               // minimum vertical distance, px
function placeEndLabels(series, x, y, width) {
  const labels = series.map((s) => {
    const last = s.values.at(-1);
    return { name: s.name, token: s.token, x: x(last.date) + 8, y: y(last.value) };
  }).sort((a, b) => a.y - b.y);

  // push labels apart top to bottom, then clamp to the plot
  for (let i = 1; i < labels.length; i++) {
    if (labels[i].y - labels[i - 1].y < LABEL_GAP) labels[i].y = labels[i - 1].y + LABEL_GAP;
  }
  return labels.map((l) => ({ ...l, y: Math.min(l.y, plotHeight - 4) }));
}

function renderLabels(g, labels) {
  g.selectAll('text.series-label').data(labels).join('text')
    .attr('class', 'series-label')
    .attr('x', (d) => d.x).attr('y', (d) => d.y).attr('dy', '0.35em')
    // SC 1.4.3: label text colour chosen for 4.5:1, echoing the series token
    .attr('fill', (d) => `var(${d.token}-text)`)
    .text((d) => d.name);
}
/* Label text tokens: darker (light theme) or lighter (dark theme) than the line colour */
:root { --series-1-text: #0d3320; --series-2-text: #6b4a00; }
[data-theme="dark"] { --series-1-text: #6ddfa0; --series-2-text: #ffd033; }
.series-label { font: 600 0.8125rem/1 Inter, system-ui, sans-serif; }

Using a separate -text token for label colour lets the line keep a mid-tone colour (fine at 3:1 as a graphic) while its label uses a darker or lighter shade that reaches 4.5:1 as text. The shared hue keeps the visual association.

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Chart exposure What screen readers get What to do
role="img" with name and description Name and description only Include series names and their trends in the description
Structured SVG with focusable points Each point’s accessible name Include the series: “Web, March, 1,240”
Canvas Nothing from labels Description + data table with series as columns
HTML legend beside the chart Legend text, if kept Mark it as a list with the same order as labels
Direct labels at line ends Mock of the right edge of a line chart where three series end, each with its name placed directly beside the last point, nudged apart to avoid collision. Direct labels at line endsSeriesLast valueLabel yNudgeWeb21,41042 pxnoneMobile1,38058 px+6 px1Retail420170 pxnone1Labels closer than 16 px are pushed apart,top to bottom2Label text uses a darker series token for4.5:1, same hue as the line
Each name sits beside its line's last point; the middle label is nudged down 6 px to clear its neighbour.

Integration context

Permalink to "Integration context"

Direct labels are one of two colour-free encodings; the other, shapes and patterns, is in patterns and shapes for colour-independent charts. Many charts benefit from both: dashes distinguish the lines along their length, labels name them at the end.

The chart’s text alternative should mention each series by the same name as its label — generating accessible text alternatives for D3 charts covers generating that summary from the data.

Placing labels without collisions Steps to place direct labels: compute each label's natural position, sort by position, push overlapping labels apart, clamp to the plot area, and add leader lines for moved labels. Placing labels without collisionsNatural positionbeside the last point of each seriesx + 8 px, y of last valueSortby vertical positiontop to bottomPush apartenforce a minimum gap between neighbours16 px at 13 px textClamp and connectkeep inside the plot; leader line if moved farthin, 3:1 line
Four steps handle most charts; leader lines cover the rest.

Gotchas

Permalink to "Gotchas"

Lines that end at different x. Series with different end dates need labels at their own last points, not aligned in a column. Check for collisions with the next series’ line, not only labels.

Long names. Reserve right-hand margin for labels; truncating series names defeats the purpose. Wrap onto two lines if needed.

Resize text at 200%. Labels in SVG scale with the chart, not with user text-size preferences. Size them in rem-derived units where the chart is HTML-overlay based, or give the chart enough size that labels remain readable at zoom.

Design system notes

Permalink to "Design system notes"

Chart components should default to direct labels when the series count is small, falling back to an HTML legend with shape and dash swatches above a threshold. Provide -text variants of each series token that meet 4.5:1 in both themes, alongside the mark colour tokens.

Testing checklist

Permalink to "Testing checklist"

FAQ

Permalink to "FAQ"
Why use direct labels instead of a legend?

Direct labels put the series name next to its data, so readers do not have to match colours between a legend and the chart. That helps colour-blind users, low-vision users at high zoom, and anyone reading quickly.

Do direct labels need to meet text contrast?

Yes. They are text, so they need 4.5:1, or 3:1 if large. Use a darker or lighter shade of the series hue for the label than for the line if the line colour itself is too light.

What if labels overlap?

Sort labels by position and push overlapping ones apart, adding thin leader lines if a label moves far from its line. If that still fails, fall back to a legend that uses shapes or dashes as well as colour.

Do screen readers read direct labels?

Only if the chart is exposed as structured content. For charts exposed as a single image, put the series names in the description and the data table instead.

Permalink to "Related"

← Back to Chart Colour & Contrast