Accessibility pattern · Data display

Accessible bar chart

The bars are drawn in SVG for the eye and exposed as a named group of images for everything else: one tab stop, the arrow keys between months, each bar saying its month and value. Values sit on the bars, the comparison year is hatched as well as listed in a legend, and the same numbers are one button away as a real table.

WCAG criteria
7
Keyboard rules
5
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Monthly sales, 2026

Sales peaked at ₹4.2 lakh in March, dipped in April and have grown every month since. Every month beat 2025 except April.

  • 2026
  • 2025, hatched
Show data table
Monthly sales in ₹ lakh, January to August
Month20262025Change from 2025
January2.82.5+12%
February3.12.7+15%
March4.23.6+17%
April2.42.6−8%
May2.92.6+12%
June3.32.8+18%
July3.63.0+20%
August3.83.1+23%

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabMoves onto the chart once, landing on the last bar you visited; the next Tab leaves the chart.
Arrow Right or Arrow LeftMoves to the next or previous month and shows its tooltip; Arrow Down and Arrow Up do the same.
Home or EndMoves to the first or last month.
EscapeHides the tooltip without moving focus; it comes back on the next bar.
Enter or SpaceOn Compare with last year, adds or removes the 2025 bars; on Show data table, opens or closes the table.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Tab reaches the chartMonthly sales, 2026, group. Sales peaked at ₹4.2 lakh in March… January: ₹2.8 lakh, image
Arrow Right reaches MarchMarch: ₹4.2 lakh, highest, image
Compare with last year is pressedCompare with last year, toggle button, pressed
A bar is reached while comparingMarch: ₹4.2 lakh, highest, image. 2025: ₹3.6 lakh, up 17%
The table is openedHide data table, button, expanded

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<figure class="ap-bar-chart" data-ap-bar-chart data-max="5" data-step="1" aria-labelledby="bar-chart-name">
  <div class="ap-bar-chart__top">
    <div class="ap-bar-chart__id">
      <h3 class="ap-bar-chart__name" id="bar-chart-name">Monthly sales, 2026</h3>
      <p class="ap-bar-chart__takeaway" id="bar-chart-takeaway">Sales peaked at ₹4.2 lakh in March, dipped in April and have grown every month since. <span class="ap-bar-chart__then">Every month beat 2025 except April.</span></p>
    </div>
    <button type="button" class="ap-btn ap-bar-chart__compare" aria-pressed="false" data-ap-compare>
      <svg class="ap-btn__icon ap-bar-chart__box" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="4" y="4" width="16" height="16" rx="4"/><path class="ap-bar-chart__tick" d="m8 12 3 3 5-6"/></svg>
      Compare with last year
    </button>
  </div>

  <ul class="ap-bar-chart__legend ap-bar-chart__then">
    <li><svg class="ap-bar-chart__swatch" viewBox="0 0 14 14" aria-hidden="true" focusable="false"><rect x="1" y="1" width="12" height="12" rx="3"/></svg>2026</li>
    <li><svg class="ap-bar-chart__swatch ap-bar-chart__swatch--last" viewBox="0 0 14 14" aria-hidden="true" focusable="false"><rect x="1" y="1" width="12" height="12" rx="3"/></svg>2025, hatched</li>
  </ul>

  <div class="ap-bar-chart__chart" data-ap-chart>
    <span class="ap-bar-chart__unit" aria-hidden="true">₹ lakh</span>
    <svg class="ap-bar-chart__svg" aria-hidden="true" focusable="false">
      <defs>
        <pattern id="bar-chart-hatch" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
          <rect class="ap-bar-chart__hatch-ground" width="6" height="6"/>
          <path class="ap-bar-chart__hatch-ink" d="M1.5 0V6"/>
        </pattern>
      </defs>
      <g data-ap-grid></g>
      <g data-ap-bars></g>
      <g data-ap-values></g>
    </svg>
    <div class="ap-bar-chart__cols" role="group" aria-labelledby="bar-chart-name" aria-describedby="bar-chart-takeaway">
      <div class="ap-bar-chart__col" role="img" tabindex="0" aria-label="January: ₹2.8 lakh" data-value="2.8" data-last="2.5" data-ap-vs="bar-chart-vs-jan">
        <span class="ap-bar-chart__tip">
          <span class="ap-bar-chart__when">January 2026</span>
          <span class="ap-bar-chart__row"><span class="ap-bar-chart__key"></span><strong>₹2.8 lakh</strong></span>
          <span class="ap-bar-chart__row ap-bar-chart__vs ap-bar-chart__then" id="bar-chart-vs-jan"><span class="ap-bar-chart__key ap-bar-chart__key--last"></span>2025: ₹2.5 lakh, up 12%</span>
        </span>
        <span class="ap-bar-chart__month">Jan</span>
      </div>
      <div class="ap-bar-chart__col" role="img" tabindex="-1" aria-label="February: ₹3.1 lakh" data-value="3.1" data-last="2.7" data-ap-vs="bar-chart-vs-feb">
        <span class="ap-bar-chart__tip">
          <span class="ap-bar-chart__when">February 2026</span>
          <span class="ap-bar-chart__row"><span class="ap-bar-chart__key"></span><strong>₹3.1 lakh</strong></span>
          <span class="ap-bar-chart__row ap-bar-chart__vs ap-bar-chart__then" id="bar-chart-vs-feb"><span class="ap-bar-chart__key ap-bar-chart__key--last"></span>2025: ₹2.7 lakh, up 15%</span>
        </span>
        <span class="ap-bar-chart__month">Feb</span>
      </div>
      <div class="ap-bar-chart__col" role="img" tabindex="-1" aria-label="March: ₹4.2 lakh, highest" data-value="4.2" data-last="3.6" data-ap-vs="bar-chart-vs-mar">
        <span class="ap-bar-chart__tip">
          <span class="ap-bar-chart__when">March 2026</span>
          <span class="ap-bar-chart__row"><span class="ap-bar-chart__key"></span><strong>₹4.2 lakh</strong></span>
          <span class="ap-bar-chart__row ap-bar-chart__vs ap-bar-chart__then" id="bar-chart-vs-mar"><span class="ap-bar-chart__key ap-bar-chart__key--last"></span>2025: ₹3.6 lakh, up 17%</span>
          <span class="ap-bar-chart__note">Highest month</span>
        </span>
        <span class="ap-bar-chart__month">Mar</span>
      </div>
      <div class="ap-bar-chart__col" role="img" tabindex="-1" aria-label="April: ₹2.4 lakh, lowest" data-value="2.4" data-last="2.6" data-ap-vs="bar-chart-vs-apr">
        <span class="ap-bar-chart__tip">
          <span class="ap-bar-chart__when">April 2026</span>
          <span class="ap-bar-chart__row"><span class="ap-bar-chart__key"></span><strong>₹2.4 lakh</strong></span>
          <span class="ap-bar-chart__row ap-bar-chart__vs ap-bar-chart__then" id="bar-chart-vs-apr"><span class="ap-bar-chart__key ap-bar-chart__key--last"></span>2025: ₹2.6 lakh, down 8%</span>
          <span class="ap-bar-chart__note">Lowest month</span>
        </span>
        <span class="ap-bar-chart__month">Apr</span>
      </div>
      <div class="ap-bar-chart__col" role="img" tabindex="-1" aria-label="May: ₹2.9 lakh" data-value="2.9" data-last="2.6" data-ap-vs="bar-chart-vs-may">
        <span class="ap-bar-chart__tip">
          <span class="ap-bar-chart__when">May 2026</span>
          <span class="ap-bar-chart__row"><span class="ap-bar-chart__key"></span><strong>₹2.9 lakh</strong></span>
          <span class="ap-bar-chart__row ap-bar-chart__vs ap-bar-chart__then" id="bar-chart-vs-may"><span class="ap-bar-chart__key ap-bar-chart__key--last"></span>2025: ₹2.6 lakh, up 12%</span>
        </span>
        <span class="ap-bar-chart__month">May</span>
      </div>
      <div class="ap-bar-chart__col" role="img" tabindex="-1" aria-label="June: ₹3.3 lakh" data-value="3.3" data-last="2.8" data-ap-vs="bar-chart-vs-jun">
        <span class="ap-bar-chart__tip">
          <span class="ap-bar-chart__when">June 2026</span>
          <span class="ap-bar-chart__row"><span class="ap-bar-chart__key"></span><strong>₹3.3 lakh</strong></span>
          <span class="ap-bar-chart__row ap-bar-chart__vs ap-bar-chart__then" id="bar-chart-vs-jun"><span class="ap-bar-chart__key ap-bar-chart__key--last"></span>2025: ₹2.8 lakh, up 18%</span>
        </span>
        <span class="ap-bar-chart__month">Jun</span>
      </div>
      <div class="ap-bar-chart__col" role="img" tabindex="-1" aria-label="July: ₹3.6 lakh" data-value="3.6" data-last="3.0" data-ap-vs="bar-chart-vs-jul">
        <span class="ap-bar-chart__tip">
          <span class="ap-bar-chart__when">July 2026</span>
          <span class="ap-bar-chart__row"><span class="ap-bar-chart__key"></span><strong>₹3.6 lakh</strong></span>
          <span class="ap-bar-chart__row ap-bar-chart__vs ap-bar-chart__then" id="bar-chart-vs-jul"><span class="ap-bar-chart__key ap-bar-chart__key--last"></span>2025: ₹3.0 lakh, up 20%</span>
        </span>
        <span class="ap-bar-chart__month">Jul</span>
      </div>
      <div class="ap-bar-chart__col" role="img" tabindex="-1" aria-label="August: ₹3.8 lakh" data-value="3.8" data-last="3.1" data-ap-vs="bar-chart-vs-aug">
        <span class="ap-bar-chart__tip">
          <span class="ap-bar-chart__when">August 2026</span>
          <span class="ap-bar-chart__row"><span class="ap-bar-chart__key"></span><strong>₹3.8 lakh</strong></span>
          <span class="ap-bar-chart__row ap-bar-chart__vs ap-bar-chart__then" id="bar-chart-vs-aug"><span class="ap-bar-chart__key ap-bar-chart__key--last"></span>2025: ₹3.1 lakh, up 23%</span>
        </span>
        <span class="ap-bar-chart__month">Aug</span>
      </div>
    </div>
  </div>

  <details class="ap-bar-chart__data">
    <summary class="ap-btn ap-btn--ghost ap-bar-chart__toggle">
      <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3.5" y="4.5" width="17" height="15" rx="2.5"/><path d="M3.5 9.5h17M3.5 14.5h17M9.5 9.5v10"/></svg>
      <span data-ap-label>Show data table</span>
    </summary>
    <div class="ap-bar-chart__scroll">
      <table class="ap-bar-chart__table">
        <caption>Monthly sales in ₹ lakh, January to August</caption>
        <thead>
          <tr><th scope="col">Month</th><th scope="col">2026</th><th scope="col" class="ap-bar-chart__then">2025</th><th scope="col" class="ap-bar-chart__then">Change from 2025</th></tr>
        </thead>
        <tbody>
          <tr><th scope="row">January</th><td>2.8</td><td class="ap-bar-chart__then">2.5</td><td class="ap-bar-chart__then">+12%</td></tr>
          <tr><th scope="row">February</th><td>3.1</td><td class="ap-bar-chart__then">2.7</td><td class="ap-bar-chart__then">+15%</td></tr>
          <tr><th scope="row">March</th><td>4.2</td><td class="ap-bar-chart__then">3.6</td><td class="ap-bar-chart__then">+17%</td></tr>
          <tr><th scope="row">April</th><td>2.4</td><td class="ap-bar-chart__then">2.6</td><td class="ap-bar-chart__then">−8%</td></tr>
          <tr><th scope="row">May</th><td>2.9</td><td class="ap-bar-chart__then">2.6</td><td class="ap-bar-chart__then">+12%</td></tr>
          <tr><th scope="row">June</th><td>3.3</td><td class="ap-bar-chart__then">2.8</td><td class="ap-bar-chart__then">+18%</td></tr>
          <tr><th scope="row">July</th><td>3.6</td><td class="ap-bar-chart__then">3.0</td><td class="ap-bar-chart__then">+20%</td></tr>
          <tr><th scope="row">August</th><td>3.8</td><td class="ap-bar-chart__then">3.1</td><td class="ap-bar-chart__then">+23%</td></tr>
        </tbody>
      </table>
    </div>
  </details>
</figure>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.1.1 Non-text Content Level A

    Every bar has a name with its month and value, the group is named by the chart's heading and described by its takeaway, and the drawing itself is hidden from assistive technology.

  • 1.3.1 Info and Relationships Level A

    The same numbers are in a table with a caption and row and column headers, so the relationships survive without the picture.

  • 1.4.1 Use of Color Level A

    Last year's bars are hatched and outlined, listed in a legend and labelled with values, so the two years never depend on telling colors apart.

  • 1.4.11 Non-text Contrast Level AA

    Both series clear 3:1 against the chart surface in light and dark themes; gridlines are kept faint on purpose because every value is also written out.

  • 1.4.13 Content on Hover or Focus Level AA

    The tooltip stays while the pointer moves onto it, stays until you leave the bar or move focus, and Escape hides it without moving anything.

  • 2.1.1 Keyboard Level A

    Every bar can be reached and read from the keyboard, and the comparison and table controls are native buttons.

  • 2.4.7 Focus Visible Level AA

    The focused month shows a two-pixel ring around its whole column, as well as the highlighted bar.

Usage

When to use it

Use it

  • Comparing amounts across a handful of categories or periods, like sales by month or tickets by team.
  • A dashboard where people want the shape at a glance and the exact number on demand.

Use something else

  • A single number that matters on its own: show it as a figure, not a one-bar chart.
  • Many points over time: a line chart reads trends better than thirty thin bars.
  • Data people mainly look up rather than compare: give them the table first.

Common failures

How it usually goes wrong

  • A picture with no words

    An SVG chart with no name is announced as "graphic" or skipped. Here each bar has a name and the group is named and described by the visible title and takeaway.

  • Series told apart by color alone

    Two shades of one hue look the same to many people and in grayscale print. Last year's bars are hatched, outlined and named in a legend.

  • Values only in the tooltip

    If hovering is the only way to read a number, touch and keyboard users miss it. Values are on the bars, in each bar's name and in the table.

  • A tooltip that vanishes when you reach for it

    Moving the pointer from the bar to its tooltip should not close it, and people who zoom need to dismiss it without moving away. It stays while hovered and closes on Escape.

  • Every bar a tab stop

    Eight bars, or eighty, in the Tab order bury whatever follows the chart. The bars share one stop and the arrow keys move between them.

  • A chart that cannot be checked

    Without the underlying numbers nobody can verify the picture or copy a value. The data table toggle shows a real table with the same figures.

Notes

Building it

  • The bars are focusable images inside a named group, not one image for the whole chart, because people asked to move between months; a static chart is better as one role=img with a name and a description.
  • The script draws the SVG from data-value and data-last on each bar and redraws when the chart resizes, so bars stay 24 pixels or thinner and the hatching keeps its angle. Generate the names, the data and the table from one source in your product.
  • Text sits in HTML over the drawing: it wraps, zooms crisply, follows text-spacing settings and can be translated. Only the numbers on the bars are SVG text.
  • Under forced colors the bars switch to CanvasText and the hatching to CanvasText on Canvas, so the two years stay distinct when the system replaces every color.
  • The bars grow in once, briefly, and not at all when the system asks for reduced motion.

Sources: WAI Images Tutorial: Complex images · WCAG 2.2: Understanding Content on Hover or Focus

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction