Observable Plot aria Attributes

Permalink to "Observable Plot aria Attributes"

Observable Plot is a concise JavaScript library for exploratory and presentation charts, built on D3 by the team behind it. It renders SVG and supports ARIA options directly: ariaLabel and ariaDescription on the plot and on marks, ariaHidden to remove decorative marks, and channel-based ariaLabel so each rendered element (a bar, a dot) can carry its own label. These options make Plot charts readable by screen readers with very little code — if you use them.

This guide belongs to chart library accessibility.

Spec reference

Permalink to "Spec reference"
  • Plot options — ariaLabel, ariaDescription on the top-level plot set aria-label and aria-description on the SVG.
  • Mark options — ariaLabel (constant or channel function per datum), ariaDescription, ariaHidden.
  • title channel — adds SVG title elements, shown as native tooltips and often read by screen readers.
  • caption — renders a figure with figcaption.
  • Relevant criteria: SC 1.1.1, 1.3.1 (A), SC 1.4.1 (A) for series styles.
Plot defaults versus aria options in use Comparison: default plots have generic or missing labels and expose decorative marks; using aria options names the chart, labels each bar or dot with values and hides grids and rules. Plot defaults versus aria options in use✗ DefaultsGeneric or missing chart labelBars without labelsGrid lines and rules exposedNo captionColour-only series✓ aria options usedariaLabel and ariaDescription on the plotariaLabel per datum with value and unitariaHidden on decorative markscaption with the main findingstroke patterns or symbols

When to use — and when not to

Permalink to "When to use — and when not to"

Use Plot’s aria options on every published chart; they cost one or two lines each. Label marks individually when there are dozens of them; for thousands, hide the marks and provide a table.

The misapplication: a dot plot of 5,000 points each with ariaLabel, creating 5,000 accessibility nodes that slow screen readers without helping anyone.

Choosing what to expose Flow: name the plot; describe its finding; label marks if there are fewer than about 100; hide decorative marks; for dense marks, hide them and provide a table. Choosing what to exposeName the plotariaLabel"Revenue by region"DescribeariaDescription and captionthe main findingFewer than ~100 marks?ariaLabel per datumvalues with unitsDecorative marksariaHidden: truegrids, rules, framesDense markshide and add a tableperformance and usability

Annotated code example

Permalink to "Annotated code example"
import * as Plot from '@observablehq/plot';

const chart = Plot.plot({
  ariaLabel: 'Revenue by region, 2026',                                   // chart name (SC 1.1.1)
  ariaDescription: 'Quarterly revenue in million euros. North grew fastest (+18%); South declined in Q3.',
  caption: 'North grew fastest (+18%); South declined in Q3.',
  x: { label: 'Quarter' },
  y: { label: 'Revenue (million euros)', grid: true },
  color: { legend: true },
  marks: [
    Plot.gridY({ ariaHidden: true }),                                      // decorative
    Plot.lineY(data, { x: 'quarter', y: 'revenue', z: 'region', stroke: 'region',
                       strokeDasharray: (d) => d.region === 'South' ? '6,4' : null }),  // dash: not colour only
    Plot.dot(data, { x: 'quarter', y: 'revenue', fill: 'region', symbol: 'region',
                     ariaLabel: (d) => `${d.region}, ${d.quarter}: ${d.revenue.toFixed(1)} million euros` }), // per-datum
    Plot.ruleY([0], { ariaHidden: true }),
  ],
});
document.querySelector('#revenue').append(chart);
renderTable('#revenue-table', data);                                       // complete data

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Element Exposed as Notes
Plot SVG Labelled graphic with description Read in browse mode
Dots with ariaLabel Labelled elements Browse mode; not keyboard focusable by default
Hidden grid and rules Not exposed Less noise
Caption figcaption text Visible and read
Table Native table Main route for details
Plot aria options and their effect Matrix of options — plot ariaLabel, plot ariaDescription, mark ariaLabel channel, mark ariaHidden, title channel, caption — against what screen reader users get. Plot aria options and their effectEffect for screen reader usersPlot ariaLabelChart namePlot ariaDescriptionChart descriptionMark ariaLabel channelPer-mark values in browse modeMark ariaHiddenLess cluttertitle channelSVG titles; tooltip textcaptionVisible summary for everyone
A few options produce a named, described, uncluttered chart.

Integration context

Permalink to "Integration context"

Plot’s per-datum labels pair well with keyboard models from keyboard navigation for scatter plots if you add focusability. Summaries follow writing chart summaries and key takeaways. Vega-Lite offers a similar declarative approach in Vega-Lite accessible output.

Gotchas

Permalink to "Gotchas"

Interactivity. Plot’s pointer interactions (tips, crosshairs) are pointer-driven; keyboard users need labels in browse mode or a table.

Re-rendering. Plot re-creates the SVG on each render; if you add focus handling, restore focus after re-render.

Legends. Colour legends are images of swatches with text; pair them with direct labels or symbols.

Worked example

Permalink to "Worked example"

A research team’s Observable notebooks fed charts into published reports. None had labels; screen reader users heard “graphic”. Adding ariaLabel and ariaDescription to the plot template, ariaHidden on grids, per-datum labels for small charts, symbols per series and a caption with the finding took an afternoon per notebook template. The team also started exporting a CSV link under each chart, which reviewers used more than expected.

A plot wrapper with defaults

Permalink to "A plot wrapper with defaults"
export function accessiblePlot({ title, summary, data, marks, ...opts }) {
  const plot = Plot.plot({ ariaLabel: title, ariaDescription: summary, caption: summary, ...opts,
    marks: marks.map(m => m), });
  const figure = document.createElement('figure');
  figure.append(Object.assign(document.createElement('h3'), { textContent: title }), plot, tableFor(data));
  return figure;
}

Grids and rules created inside the wrapper set ariaHidden: true, and mark labels are added only when the data has fewer than about a hundred rows.

Testing it with a screen reader

Permalink to "Testing it with a screen reader"
  1. Browse to the chart; confirm name and description.
  2. Explore marks; confirm labels with units for small charts.
  3. Confirm grids and rules are not announced.
  4. Check greyscale for series distinction.
  5. Read the table.

Common mistakes

Permalink to "Common mistakes"
  • No ariaLabel or description.
  • Labelling thousands of marks.
  • Exposing decorative marks.
  • Colour-only series.

Adoption checklist for teams

Permalink to "Adoption checklist for teams"
  1. Add aria options to a shared plot wrapper.
  2. Hide decorative marks by default.
  3. Label marks only below a count threshold.
  4. Use symbols or dashes per series.
  5. Generate a table and caption for every published chart.

FAQ

Permalink to "FAQ"
Does Observable Plot support ARIA?

Yes, through ariaLabel, ariaDescription and ariaHidden options on plots and marks.

Are marks keyboard focusable?

Not by default. Add focus handling yourself if users need keyboard exploration, or rely on tables.

How many marks should be labelled?

Up to roughly a hundred. Beyond that, hide them and provide a table and summary.

Do title channels help?

They add SVG titles that some readers announce and that show as tooltips; they are not a substitute for ariaLabel.

How do I describe the chart's finding?

Use ariaDescription and a visible caption with the same message.

Can I use Plot in React or Vue?

Yes; render the SVG into a container element and manage re-rendering and focus.

Does Plot support dark mode?

Pass theme-aware colours and check contrast in both themes.

How do I add keyboard exploration to a Plot chart?

Make the SVG focusable, keep an index into the data, highlight the current mark with a separate mark layer, and announce values through a status region on arrow keys.

How do I handle faceted plots?

Give each facet a meaningful label and include facet values in mark labels and the table; consider summarising each facet in the caption.

Do Plot legends support keyboard toggling?

No; build an HTML toggle group if users need to show and hide series.

Permalink to "Related"

← Back to Chart Library Accessibility