Accessibility pattern · Data display

Accessible line chart

The plot is a slider over the days: the arrow keys move a crosshair, the date is its value, and the numbers for that day follow in a polite announcement once you stop. Two series are told apart by a solid and a dashed line as well as a legend, the highest and lowest days are labelled on the chart, and the table it is drawn from is one button away.

WCAG criteria
7
Keyboard rules
6
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.

Daily visitors, September 2026

The 7-day average rose 38% over the month, and visitors peaked at 2,640 on 24 September.

  • Daily visitors
  • 7-day average, dashed
Highest Lowest visitors 7-day average

Arrow keys move a day, Page Up and Page Down a week.

Show data table
Daily visitors, 1 to 30 September 2026
DateVisitors7-day average
1,6101,430
1,6301,440
1,6201,470
1,6501,490
1,3501,500
Lowest1,3001,520
1,6001,540
1,6801,550
1,7401,560
1,7601,580
1,6801,590
1,4501,600
1,3501,610
1,8001,640
1,8601,660
1,7701,670
1,8501,680
1,9301,720
1,5401,730
1,4601,740
1,8701,750
1,9901,770
2,0101,810
Highest2,6401,920
1,9501,920
1,6601,940
1,5101,950
1,9201,950
2,0101,960
2,1201,970

The 7-day average for 1 to 6 September includes the last days of August.

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, showing the crosshair on the day it was last on; the next Tab moves to the table toggle.
Arrow Right or Arrow LeftMoves the crosshair to the next or previous day; Arrow Up and Arrow Down do the same.
Page Up or Page DownMoves a week later or earlier, as Page Up raises a slider's value.
Home or EndMoves to 1 or 30 September.
EscapeHides the crosshair and tooltip without moving focus; the next key brings them back.
Enter or SpaceOn 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 chartDaily visitors, September 2026, slider, Tuesday, 1 September. Arrow keys move a day, Page Up and Page Down a week.
Arrow Right is pressedWednesday, 2 September
The keys stop for a moment1,630 visitors, 7-day average 1,440
End is pressed, then Page DownWednesday, 23 September… 2,010 visitors, 7-day average 1,810
The crosshair reaches 24 SeptemberThursday, 24 September… 2,640 visitors, 7-day average 1,920. Highest of the month

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-line-chart" data-ap-line-chart data-max="3000" data-step="500" data-region="IN" aria-labelledby="line-chart-name">
  <h3 class="ap-line-chart__name" id="line-chart-name">Daily visitors, September 2026</h3>
  <p class="ap-line-chart__takeaway" id="line-chart-takeaway">The 7-day average rose 38% over the month, and visitors peaked at 2,640 on 24&nbsp;September.</p>

  <ul class="ap-line-chart__legend">
    <li><svg class="ap-line-chart__key" viewBox="0 0 24 12" aria-hidden="true" focusable="false"><path d="M2 6h20"/></svg>Daily visitors</li>
    <li><svg class="ap-line-chart__key ap-line-chart__key--avg" viewBox="0 0 24 12" aria-hidden="true" focusable="false"><path d="M2 6h20"/></svg>7-day average, dashed</li>
  </ul>

  <div class="ap-line-chart__chart" data-ap-chart>
    <svg class="ap-line-chart__svg" aria-hidden="true" focusable="false">
      <g data-ap-grid></g>
      <path class="ap-line-chart__area" data-ap-area/>
      <path class="ap-line-chart__line ap-line-chart__line--avg" data-ap-avg/>
      <path class="ap-line-chart__line" data-ap-daily/>
      <g data-ap-notes></g>
      <line class="ap-line-chart__cross" data-ap-cross/>
      <rect class="ap-line-chart__dot ap-line-chart__dot--avg" width="9" height="9" rx="1.5" data-ap-dot-avg/>
      <circle class="ap-line-chart__dot" r="5" data-ap-dot/>
    </svg>
    <div class="ap-line-chart__plot" role="slider" tabindex="0" aria-labelledby="line-chart-name" aria-describedby="line-chart-keys" aria-valuemin="1" aria-valuemax="30" aria-valuenow="1" aria-valuetext="Tuesday, 1 September" data-ap-plot>
      <span class="ap-line-chart__mark" data-ap-high><span>Highest</span> <strong data-ap-num></strong></span>
      <span class="ap-line-chart__mark ap-line-chart__mark--low" data-ap-low><span>Lowest</span> <strong data-ap-num></strong></span>
      <span class="ap-line-chart__tip" data-ap-tip>
        <span class="ap-line-chart__when" data-ap-when></span>
        <span class="ap-line-chart__row"><svg class="ap-line-chart__key" viewBox="0 0 24 12" aria-hidden="true" focusable="false"><path d="M2 6h20"/></svg><strong data-ap-daily-value></strong> <span>visitors</span></span>
        <span class="ap-line-chart__row"><svg class="ap-line-chart__key ap-line-chart__key--avg" viewBox="0 0 24 12" aria-hidden="true" focusable="false"><path d="M2 6h20"/></svg><strong data-ap-avg-value></strong> <span>7-day average</span></span>
        <span class="ap-line-chart__flag" data-ap-flag="high" hidden>Highest of the month</span>
        <span class="ap-line-chart__flag" data-ap-flag="low" hidden>Lowest of the month</span>
      </span>
    </div>
    <div class="ap-line-chart__x" aria-hidden="true">
      <span data-day="1">1 Sept</span>
      <span data-day="8">8 Sept</span>
      <span data-day="15">15 Sept</span>
      <span data-day="22">22 Sept</span>
      <span data-day="29">29 Sept</span>
    </div>
  </div>
  <p class="ap-line-chart__keys" id="line-chart-keys">Arrow keys move a day, Page Up and Page Down a week.</p>
  <p class="ap-line-chart__sr" aria-live="polite" data-ap-live></p>
  <span class="ap-line-chart__sr" data-ap-say hidden>{visitors} visitors, 7-day average {average}</span>

  <details class="ap-line-chart__data">
    <summary class="ap-btn ap-btn--ghost ap-line-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-line-chart__scroll" tabindex="0" role="region" aria-labelledby="line-chart-caption">
      <table class="ap-line-chart__table" data-ap-table>
        <caption id="line-chart-caption">Daily visitors, 1 to 30 September 2026</caption>
        <thead>
          <tr><th scope="col">Date</th><th scope="col">Visitors</th><th scope="col">7-day average</th></tr>
        </thead>
        <tbody>
          <tr data-date="2026-09-01"><th scope="row"><time datetime="2026-09-01">Tue 1 Sept</time></th><td>1,610</td><td>1,430</td></tr>
          <tr data-date="2026-09-02"><th scope="row"><time datetime="2026-09-02">Wed 2 Sept</time></th><td>1,630</td><td>1,440</td></tr>
          <tr data-date="2026-09-03"><th scope="row"><time datetime="2026-09-03">Thu 3 Sept</time></th><td>1,620</td><td>1,470</td></tr>
          <tr data-date="2026-09-04"><th scope="row"><time datetime="2026-09-04">Fri 4 Sept</time></th><td>1,650</td><td>1,490</td></tr>
          <tr data-date="2026-09-05"><th scope="row"><time datetime="2026-09-05">Sat 5 Sept</time></th><td>1,350</td><td>1,500</td></tr>
          <tr data-date="2026-09-06"><th scope="row"><time datetime="2026-09-06">Sun 6 Sept</time> <span class="ap-line-chart__tag">Lowest</span></th><td>1,300</td><td>1,520</td></tr>
          <tr data-date="2026-09-07"><th scope="row"><time datetime="2026-09-07">Mon 7 Sept</time></th><td>1,600</td><td>1,540</td></tr>
          <tr data-date="2026-09-08"><th scope="row"><time datetime="2026-09-08">Tue 8 Sept</time></th><td>1,680</td><td>1,550</td></tr>
          <tr data-date="2026-09-09"><th scope="row"><time datetime="2026-09-09">Wed 9 Sept</time></th><td>1,740</td><td>1,560</td></tr>
          <tr data-date="2026-09-10"><th scope="row"><time datetime="2026-09-10">Thu 10 Sept</time></th><td>1,760</td><td>1,580</td></tr>
          <tr data-date="2026-09-11"><th scope="row"><time datetime="2026-09-11">Fri 11 Sept</time></th><td>1,680</td><td>1,590</td></tr>
          <tr data-date="2026-09-12"><th scope="row"><time datetime="2026-09-12">Sat 12 Sept</time></th><td>1,450</td><td>1,600</td></tr>
          <tr data-date="2026-09-13"><th scope="row"><time datetime="2026-09-13">Sun 13 Sept</time></th><td>1,350</td><td>1,610</td></tr>
          <tr data-date="2026-09-14"><th scope="row"><time datetime="2026-09-14">Mon 14 Sept</time></th><td>1,800</td><td>1,640</td></tr>
          <tr data-date="2026-09-15"><th scope="row"><time datetime="2026-09-15">Tue 15 Sept</time></th><td>1,860</td><td>1,660</td></tr>
          <tr data-date="2026-09-16"><th scope="row"><time datetime="2026-09-16">Wed 16 Sept</time></th><td>1,770</td><td>1,670</td></tr>
          <tr data-date="2026-09-17"><th scope="row"><time datetime="2026-09-17">Thu 17 Sept</time></th><td>1,850</td><td>1,680</td></tr>
          <tr data-date="2026-09-18"><th scope="row"><time datetime="2026-09-18">Fri 18 Sept</time></th><td>1,930</td><td>1,720</td></tr>
          <tr data-date="2026-09-19"><th scope="row"><time datetime="2026-09-19">Sat 19 Sept</time></th><td>1,540</td><td>1,730</td></tr>
          <tr data-date="2026-09-20"><th scope="row"><time datetime="2026-09-20">Sun 20 Sept</time></th><td>1,460</td><td>1,740</td></tr>
          <tr data-date="2026-09-21"><th scope="row"><time datetime="2026-09-21">Mon 21 Sept</time></th><td>1,870</td><td>1,750</td></tr>
          <tr data-date="2026-09-22"><th scope="row"><time datetime="2026-09-22">Tue 22 Sept</time></th><td>1,990</td><td>1,770</td></tr>
          <tr data-date="2026-09-23"><th scope="row"><time datetime="2026-09-23">Wed 23 Sept</time></th><td>2,010</td><td>1,810</td></tr>
          <tr data-date="2026-09-24"><th scope="row"><time datetime="2026-09-24">Thu 24 Sept</time> <span class="ap-line-chart__tag">Highest</span></th><td>2,640</td><td>1,920</td></tr>
          <tr data-date="2026-09-25"><th scope="row"><time datetime="2026-09-25">Fri 25 Sept</time></th><td>1,950</td><td>1,920</td></tr>
          <tr data-date="2026-09-26"><th scope="row"><time datetime="2026-09-26">Sat 26 Sept</time></th><td>1,660</td><td>1,940</td></tr>
          <tr data-date="2026-09-27"><th scope="row"><time datetime="2026-09-27">Sun 27 Sept</time></th><td>1,510</td><td>1,950</td></tr>
          <tr data-date="2026-09-28"><th scope="row"><time datetime="2026-09-28">Mon 28 Sept</time></th><td>1,920</td><td>1,950</td></tr>
          <tr data-date="2026-09-29"><th scope="row"><time datetime="2026-09-29">Tue 29 Sept</time></th><td>2,010</td><td>1,960</td></tr>
          <tr data-date="2026-09-30"><th scope="row"><time datetime="2026-09-30">Wed 30 Sept</time></th><td>2,120</td><td>1,970</td></tr>
        </tbody>
      </table>
    </div>
    <p class="ap-line-chart__foot">The 7-day average for 1 to 6 September includes the last days of August.</p>
  </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

    The chart is named by its heading and summed up by a visible takeaway; every day's numbers can be read with the crosshair or in the table.

  • 1.3.1 Info and Relationships Level A

    The data is a real table with a caption and headers, and the drawing is generated from it, so the two cannot disagree.

  • 1.4.1 Use of Color Level A

    The 7-day average is dashed as well as a different color, and the legend says so in words.

  • 1.4.11 Non-text Contrast Level AA

    Both lines and their markers clear 3:1 against the surface in both themes, and under forced colors they are redrawn in CanvasText.

  • 1.4.13 Content on Hover or Focus Level AA

    The tooltip can be hovered without moving, stays until the pointer or focus leaves the plot, and Escape hides it.

  • 2.1.1 Keyboard Level A

    Every day can be reached with the arrow keys, Page Up and Page Down, Home and End, the keys people expect from a slider.

  • 4.1.3 Status Messages Level AA

    Each day's numbers are announced through a polite live region, once the keys pause, without moving focus.

Usage

When to use it

Use it

  • Change over time with many points, like daily visitors, temperatures or prices over a month.
  • Comparing a raw series with its trend, or two measures that share one unit and one axis.

Use something else

  • Two measures with different units: draw two charts rather than a second axis.
  • A few categories with no order in time: use a bar chart.
  • A single latest value that matters more than the history: show the number, with the line as a small sparkline.

Common failures

How it usually goes wrong

  • Thirty tab stops

    Making every point focusable puts thirty stops between the chart and the rest of the page. One focusable plot with arrow keys inside it is faster for everyone.

  • A live region that floods

    Announcing every point as the key repeats queues a minute of speech. The date comes from the slider at once and the numbers only after the keys pause.

  • A second axis

    Two y-axes invite comparisons the data does not support, and a screen reader user cannot see which line belongs to which. Both series here share one axis and one unit.

  • Series told apart by hue

    Two lines in similar colors merge for many people and disappear in forced colors. The average is dashed and named in the legend, and the lines turn CanvasText in forced colors.

  • Peaks you have to find yourself

    The highest and lowest days are the story; leaving people to hunt for them by hovering hides it. They are labelled on the chart, flagged in the table and stated in the takeaway.

  • A tooltip that runs away

    A tooltip beside a moving crosshair slides off as the pointer heads for it, and one over the point hides the line. This one sits above the plot over the crosshair: moving straight up keeps the same day, and Escape hides it.

Notes

Building it

  • role=slider is deliberate: it makes screen readers pass the arrow keys through, and Home, End, Page Up and Page Down are the keys a slider already promises. The value is the date, so it is short to hear.
  • The live region is written 450 milliseconds after the last key, so holding an arrow down gives one announcement when you stop, not one per day.
  • The script reads the table, draws the SVG at the chart's real size and redraws when it resizes, so lines stay two pixels wide and markers stay round.
  • Dates are formatted with Intl.DateTimeFormat in the page's language, and the sentence it announces is a template in the markup, so a translation can reorder its words.
  • The area under the daily line is a light wash of the series color; under forced colors it is removed and both lines become CanvasText.

Sources: WAI-ARIA Authoring Practices: Slider · WAI Images Tutorial: Complex images

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