Search topics
Charts whose numbers a screen reader can't read
A bar chart is drawn on a canvas with no text in it, or named only "Orders chart", so a screen reader skips it or reads the name and never a number.
<canvas id="orders"></canvas> The problem
Screen reader users can't learn what a chart shows: not its finding, and not one of its numbers.
A shop's dashboard opens with a card headed "Orders per day". Under the heading, a line says "Last week", and a bar chart shows one bar for each day, Monday to Sunday. The seller can see at a glance that Saturday was the busiest day, with 31 orders, and that Monday was the quietest, with 12.
A screen reader reading down the card says "Orders per day, heading" and "Last week", and then moves on to whatever comes after the card. The chart isn't mentioned at all. In a second common build, the chart has been given a name, and the screen reader now says "Orders chart, image". The listener knows a chart is there, but not what it shows: not the busiest day, and not one of the seven counts.
The chart is drawn by Chart.js on a canvas element, as in the library's getting-started example, <canvas id="myChart"></canvas>. A canvas is a surface for pixels. The bars, the day labels and the numbers on the axis are painted onto it, and none of them exists as text the browser can pass to a screen reader. Chart.js's own documentation says so: the canvas content will not be accessible to screen readers, and the text alternative has to be added by the page, with ARIA on the canvas or with fallback content inside it. The empty canvas has neither, so there is nothing to read.
1.1.1 Non-text Content asks that every piece of non-text content that tells something has a text alternative that serves the same purpose. A chart is there to show its data and what the data says, so its text alternative has to say that. The second build adds role="img" and aria-label="Orders chart". The chart now has a name, and that is all it has: the name says what the thing is, not what it tells. Highcharts does much the same without its accessibility module, naming the chart's SVG with its title alone. A name like this fails 1.1.1 just as surely as no name does, because nothing in it serves the purpose the chart serves.
A dashboard is where a seller checks how the week went, and the chart is often the only place on the page where those counts appear.
2 of 10 groups affected
- No vision (affected)
- Low vision (affected)
- Colour vision (not affected)
- No hearing (not affected)
- Hard of hearing (not affected)
- No speech (not affected)
- Motor (not affected)
- Reach, strength (not affected)
- Cognitive (not affected)
- Seizures (not affected)
- Without vision A screen reader reads the card's heading and its date range, then nothing for the chart, or only "Orders chart, image". Which day was busiest, and every day's count, never reach the listener, and a braille display shows nothing for them either.
- Limited vision People who magnify the page see only part of the chart at once, and read each count by following a bar's top across to the gridlines, which is slow and easy to get wrong. With no text, the bars are all they have.
axe has no rule for a canvas with no text, so the first build passes axe. Its role-img-alt rule reports an element with role="img" and no name, which catches react-chartjs-2, since its canvas sets role="img" and gives no name unless one is passed. Adding aria-label="Orders chart" silences that rule, and so does any name at all, however little it says. A quick fix written by hand or by an AI assistant can do just that. This audit recognises charts drawn by Chart.js, Highcharts, Recharts, ApexCharts, ECharts and Plotly. It reports one as a finding when the chart has no text alternative at all, or a name that is only a word such as "chart", and no table beside it. Every other chart it finds becomes a lead, quoting the name or caption it has, for a person to check against the bars. Only someone who can see the chart can tell whether a name says what the bars show.
Try it
Cover the chart, then find the busiest day from what is read out.
Broken 1: the panel shows what a screen reader reads, from the top of the card. Does any of it come from the chart?
Broken 2: the chart now has a name. Does the name tell you which day was busiest?
Apply a fix. With the data in the name, every count is read with the chart. With the table, also open “Show the numbers” and read the rows, with the chart covered or not.
Broken Can you fix this?
Orders per day
Reading the card from the top, a screen reader says:
- Orders per day, heading
- Last week
The fix
Say what the chart shows, and give its numbers as text.
A chart drawn on a canvas with nothing to read
<section class="card"> <h2>Orders per day</h2> <p class="range">Last week</p> <canvas id="orders"></canvas></section>What is wrong2 points
- The bars, the day labels and the axis are painted as pixels, so none of them can be read.
- The canvas has no name and nothing inside it, so a screen reader passes it without a word.
Write the data into the chart's name
<section class="card"> <h2>Orders per day</h2> <p class="range">Last week</p> <canvas id="orders" role="img"></canvas></section>How it is fixed3 steps
- Give the canvas
role="img", so it is read as one image with a name. A canvas has no role of its own that takes a name. - Build the name from the same two arrays the chart is drawn from: what the chart shows, its main finding, then every day with its count. A screen reader now says "Orders per day, last week. Busiest on Saturday, with 31. Monday 12, Tuesday 15, …, Sunday 20, image".
- Set the name in the code that draws the chart, so a new week's data gives a new name.
This fixes both Befores and passes 1.1.1 for a chart this small: seven values fit in a sentence a listener can follow. The numbers reach only screen reader users, though, and as one string, which can't be paused on Thursday or read again from Friday. A chart with dozens of points, or several series, needs the table in the next fix.
Both fixes pass 1.1.1 on this chart. Writing the data into the name is the smallest change, one attribute and a function, and it suits a small chart in a tight space, such as a card with a handful of bars. Its numbers reach only screen reader users, read as one string, and the string grows with every bar. The table costs a few more lines and a closed disclosure under the chart, and in return it works for a chart of any size, lets a listener step through the days and come back to one, and gives exact counts to sighted people who'd rather read than estimate. That makes it the fix to start with, since the name-only fix stops working as soon as the chart grows.
What the name says. Write what the chart is there to tell: what it measures, over what period, and the one thing a reader should take from it. "Busiest on Saturday, with 31" is what the seller looks at the chart for. Leave out how it is drawn ("bar chart with blue bars"), and don't start with "chart of": the screen reader already says "image".
Charts whose data changes. A dashboard redraws its chart when the range changes, a filter is applied or new orders come in. Set the name and the table rows again in the same function that calls chart.update(), so the text never describes last week's bars. A name written once in the HTML, beside a chart redrawn by a script, is W3C's failure F20.
aria-describedby and aria-details. Pointing aria-describedby at the table reads it as one flat run of words, with its rows and headers lost, so it suits a sentence, not a table. aria-details points at the table without flattening it, but screen readers differ in whether they say that details exist and how a person gets to them. Neither is needed here: the table sits right after the chart, and the name says where it is.
A table that is always shown. On a report page, or anywhere the exact numbers matter, show the table without a details around it, or offer a switch between the chart and the table. Either way, it is the same table, built from the same data.
Chart libraries' own features. Highcharts' accessibility module, loaded as accessibility.js, adds descriptions and keyboard access to the chart, and its export-data module can show the chart's data as a table. Recharts' accessibilityLayer, on by default since version 3.0, adds keyboard access to the chart. Use them where they help, and check what a screen reader actually hears: a library's description is built from its settings, and it still needs the title and finding you give it.
When the chart only repeats the text. If the numbers are already on the page as text, such as a table the chart sits beside, the chart adds nothing for a screen reader. It still needs a short name that says what it shows and that the numbers are in the table, or it can be hidden with aria-hidden="true". A chart's SVG left with no role and no name is handled differently from one screen reader to another, skipped by some and announced as a group by others, and the text drawn in it, such as the axis labels, can be read out as stray words.
Series told apart only by colour, such as two lines in blue and orange with a legend, are a separate failure of 1.4.1 Use of Color, which order statuses shown only by colour covers on a status marker. A chart exported as a picture, such as a PNG from a spreadsheet, needs the same text alternative. Its labels are not images of text under 1.4.5, which leaves out text in a picture with significant other visual content, such as a graph, but a table of figures drawn into an image is, as size charts and notices drawn into images explains.
Design ideas
A chart can carry its numbers in words.
Four ways to draw last week’s orders with the numbers there as text, and one dashboard tile that gives only a line. Try the Chart and Table switch.
In the design file
- Write the chart’s title as what it shows, such as “Saturday was the busiest day”, with what is counted under it.
- Put every number the chart shows where it can be read as text, on the bars, in a table view or both.
- Write the sentence a screen reader should hear for the chart, with what it shows, the main finding and where the numbers are.
- Mark which words and figures come from the data, so they are built from the same numbers as the bars.
Passes
- Counts written on the bars Each bar runs along its day and ends in its count, set as real text in #1f2933, 14.76:1 on white. A screen reader reads the list as “Monday 12”, and nobody has to follow a bar across to a gridline.
Saturday was the busiest day
Orders per day, last week
The finding as the title “Saturday was the busiest day” says, in words everyone reads before the bars, what the chart is there to show, with “Orders per day, last week” in #6b7280 grey, 4.83:1, under it. Write the title from the data, so it changes with the week.- A Chart and Table switch Two buttons above the plot swap the bars for a table of the same counts, which anyone can read, zoom or copy. Draw the table view with as much care as the chart, and the switch with its chosen side filled.
- Orders last week 134 Up 18% on the week beforeThe figure first, the line after A dashboard tile leads with its figure, “134”, and “Up 18% on the week before” in #1f6b3a green, 6.52:1, with an arrow, and the sparkline under them only repeats that in a shape. The line can then be hidden from a screen reader, since the words already say it.
Fails
- OrdersA sparkline with no figureWCAG 1.1.1 The tile holds “Orders” and a line, and nothing else, so the design gives no words or figure for what the line shows. The rise reaches only people who can see it, unless a developer makes up the words. Write the figure and the change on the tile.
Verify the fix
5 checks, no mouse.
Test with: Screen reader, Zoom
Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat whenever the chart component or its data source changes.
Limits
This is a starting pattern, not a guarantee for every chart. Chart is Chart.js, and the same text alternative applies to a chart drawn by any library or by hand. This guide covers a chart's text alternative. A chart whose points, legend or tooltips do something only for a pointer, such as a value shown only on hover or a bar that filters the page when clicked, is a different failure: a legend is covered in chart legends a keyboard can't toggle, and a value shown only on hover in chart tooltips a keyboard can't open. For an image that isn't a chart, see images that tell something but have no alt text. Test the complete dashboard with your target assistive technology.
This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.
References
- WCAG Understanding: non-text-content (opens in a new tab)
- WAI Tutorial: images/complex (opens in a new tab)
- WCAG Technique F20 (opens in a new tab)
- WCAG Technique F67 (opens in a new tab)
- chartjs.org: accessibility (opens in a new tab)
- highcharts.com: accessibility-module (opens in a new tab)
- github.com: v3.0.0 (opens in a new tab)
EaseWeb service
Fixed it? Keep it from coming back.
I turn guides like this one into rules your coding agents follow and checks your CI runs. Then I retest by hand with a keyboard and screen readers, periodically.