Describing a Chart with aria-roledescription

A screen reader lands on your chart and announces “image” — technically accurate, and useless, because the user learns nothing about what kind of chart it is or what it shows.

You can do better with aria-roledescription, which lets you replace the generic role announcement with “bar chart” while keeping the underlying role="img" semantics that assistive technology relies on. This page is part of screen-reader-friendly charts, and it covers how to name a chart’s type precisely, how to structure the labels around it, and — the decision people get wrong — when a figure/figcaption pairing serves the user better than an img role. Used carelessly, aria-roledescription degrades the experience; used well, it gives orientation the generic role cannot.

Diagnostic checklist

What a screen reader announces with and without a role description Without aria-roledescription a chart is announced as image, while with it the same chart is announced as bar chart followed by its name. role="img" only announced: "image" + aria-roledescription announced: "Revenue by quarter, bar chart"
aria-roledescription replaces the word "image" with "bar chart" while the underlying role="img" keeps the element a single, well-behaved accessible object.
Colour plus redundant shape encoding Four categories encoded by hue and a repeated shape, each clearing 3:1 contrast. Categorical encoding — colour + redundant cueSeries A4.9:1Series B4.8:1Series C5.2:1Series D5.8:1Shape repeats the category so colour is never the only signaland each hue clears 3:1 against the background
The encoding principle for a chart with aria: pair every hue with a shape so the category reads without colour, and keep each swatch above 3:1.

Broken vs fixed

// ❌ BROKEN: role description on a role-less div, and no accessible name.
function labelChart(el: HTMLElement): void {
  // aria-roledescription REQUIRES a valid role to attach to; on a bare div
  // many screen readers ignore it entirely, so this announces nothing useful.
  el.setAttribute("aria-roledescription", "barchart"); // also a code token, not a phrase
}
// ✅ FIXED: valid role, a real name, and a localised role description.
interface ChartLabel {
  name: string;            // e.g. "Revenue by quarter"
  typeLabel: string;       // localised, e.g. "bar chart"
  descriptionId?: string;  // id of a visually hidden summary element
}

function labelChart(el: SVGElement | HTMLElement, label: ChartLabel): void {
  // A11Y: role=img collapses the chart into ONE accessible object so a screen
  // reader does not walk every <rect>; it announces the chart as a unit.
  el.setAttribute("role", "img");
  // A11Y: the accessible NAME — what the chart is about (WCAG 4.1.2 Name/Role/Value).
  el.setAttribute("aria-label", label.name);
  // A11Y: replaces the generic "image" with a human, localised type phrase.
  // Screen readers announce: name + roledescription → "Revenue by quarter, bar chart".
  el.setAttribute("aria-roledescription", label.typeLabel);
  if (label.descriptionId) {
    // A11Y: longer trend detail lives in a described-by element, not the label.
    el.setAttribute("aria-describedby", label.descriptionId);
  }
}

Three rules keep aria-roledescription from backfiring. It only works on an element with a genuine ARIA role, so pair it with role="img" (or another appropriate role) rather than a bare container. It replaces the role announcement but not the name, so the element still needs aria-label or aria-labelledby — a role description with no name announces “bar chart” and nothing about the subject. And its value must be a localised, human phrase, because screen readers read it verbatim: “bar chart” is right, “barchart-v2” is a bug a user will hear.

When to use role=“img” versus a figure element

The two approaches answer different questions, and choosing correctly matters more than the role description itself.

Use Reach for Because
Static chart with a visible caption <figure> + <figcaption> The caption is a real, visible, associated description; users can read or hear it, and it needs no ARIA
Interactive chart collapsed to one announcement role="img" + aria-roledescription Stops AT from walking every mark; names the type; pairs with a data table for detail
Chart whose marks must each be focusable container role + per-mark labels role="img" hides descendants from AT, which is wrong when points must be reachable
Complex, explorable data structure role="figure" or a table structure Keeps children exposed while still grouping and naming the whole

The trap in the table’s third row is the most consequential: role="img" prunes the element’s descendants from the accessibility tree. That is exactly what you want for a static chart you want announced as a single object with a text alternative — but it is exactly wrong for an interactive chart whose individual points must be focusable and labelled, because the img role hides those very points. So the decision is really about interactivity. If the chart is a static picture, role="img" (optionally inside a <figure>) plus a good name and a role description is ideal. If the chart is explorable mark by mark, do not put role="img" on the container — use role="figure" or a grouping structure that keeps the marks exposed, and apply names and role descriptions to the marks and the group rather than collapsing everything.

Step-by-step fix

// Build a captioned, static chart with a figure and a role description.
function renderCaptionedChart(host: HTMLElement, svg: SVGElement, name: string, caption: string): void {
  const fig = document.createElement("figure");
  const cap = document.createElement("figcaption");
  cap.textContent = caption;                     // visible, real description
  svg.setAttribute("role", "img");
  svg.setAttribute("aria-label", name);          // accessible name
  svg.setAttribute("aria-roledescription", "bar chart"); // localised type phrase
  fig.append(svg, cap);
  host.appendChild(fig);
}

Verification

// Assert the chart has a role, a name, and a human role description.
const el = document.querySelector<SVGElement>("svg[role='img']")!;
console.assert(el.getAttribute("role") === "img", "chart needs a valid role for roledescription to apply");
const name = el.getAttribute("aria-label") || el.getAttribute("aria-labelledby");
console.assert(!!name, "aria-roledescription does not replace the accessible name");
const rd = el.getAttribute("aria-roledescription") || "";
console.assert(/\s/.test(rd) && rd === rd.toLowerCase(), "role description should be a human phrase, not a code token");

In DevTools, select the chart in the Accessibility pane and confirm the computed Role is “img” while the announced role is your description; the Name field must be populated separately. Then test with a real screen reader: it should say the name followed by the role description (“Revenue by quarter, bar chart”), and — for a static chart — it should not let the user walk into the individual bars.

Why the role description does not replace the values

aria-roledescription is orientation, not content. It tells the user what kind of object they have reached and what it is about, which is a real improvement over “image”, but it says nothing about the data itself — the numbers, the trend, the outliers. A chart that announces “Revenue by quarter, bar chart” and then offers no way to reach the values is still inaccessible; the user knows the genre of the thing they cannot read. So a role description is always the top layer of a stack, never the whole answer. Underneath it belong the concrete channels that carry the data: a generated summary referenced with aria-describedby for the gist, and an accessible data table for exact values, as covered in exposing chart data as an accessible data table. Think of the role description as the label on a filing cabinet: helpful for finding the right drawer, but the documents inside still have to exist.

Edge cases & gotchas

  • Role description on a role-less element. Applied to a bare <div> or <span> with no ARIA role, aria-roledescription is ignored by many screen readers. Always pair it with a valid role such as img.
  • Losing the accessible name. aria-roledescription replaces the role phrase but not the name. Without aria-label/aria-labelledby the user hears the type with no subject — set both.
  • Code tokens read aloud. Screen readers speak the value verbatim, so an internal identifier like “chart_bar_v2” is read out literally. Use a localised, natural phrase and translate it per locale.
  • role="img" hiding interactive marks. Because the img role prunes descendants, putting it on a chart whose points must be focusable makes those points unreachable. Use role="figure" or a grouping structure for interactive charts.
  • Over-describing the type. “vertical clustered stacked bar chart with error bars” is more than a user wants read on every visit. Keep the role description short (“bar chart”) and put the finer structure in the summary.
  • Redundant announcements. If you use <figure> with a visible <figcaption> and an aria-label saying the same thing, some readers announce both. Let the figcaption be the description and keep the label distinct, or reference the caption with aria-labelledby to avoid duplication.
Bars encoded by texture, not only colour Four bars distinguished by pattern fills as well as hue. valueNorthSouthEastWesttexture repeats the series identity beyond colour
Redundant texture on each bar for a chart with aria — the pattern carries series identity even where colour perception fails.

Frequently Asked Questions

Does aria-roledescription work on any element?

No. It only takes effect on an element that already has a valid ARIA role, either implicit or explicit. On a role-less <div> or <span> it is widely ignored, so pair it with something like role="img" for a static chart. It also never supplies the accessible name — you still need aria-label or aria-labelledby alongside it.

When should I use figure and figcaption instead of role=“img”?

Use <figure>/<figcaption> when the chart has a caption you want visible on screen, because the figcaption is a genuine, associated description that everyone can read and assistive technology exposes without extra ARIA. Reach for role="img" with aria-roledescription when you want an interactive-but-static chart collapsed into a single announced object with a text alternative. If the chart’s individual marks must be focusable, avoid role="img" on the container entirely, since it hides those marks.

Is naming the chart type enough to make it accessible?

No. aria-roledescription tells the user what kind of chart it is, not what the data says. It is the orientation layer and must sit on top of real content channels — a generated summary via aria-describedby and an accessible data table for the values. Announcing “bar chart” while providing no way to reach the numbers still leaves the chart unreadable.