Command Palette

Search for a command to run...

Hectal
PHASE 15Advanced ~34 min· topic 4 of 7

Topic 15.4

Keyboard Navigation Through Data

In one line

People who cannot use a mouse need a way to walk through a chart's data with the keyboard. Make the chart one Tab stop, move with the arrow keys, show where focus is, and say each value aloud with a slider's text or a live region.

Think of it like this

A lift with a panel of 25 buttons. If you must press every button in turn to reach floor 25, the lift is no use. A good panel lets you step to the panel once and then jump to the floor you want. A chart with 25 bars should be one stop, with the arrow keys moving between the bars.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

tab stop
An element the Tab key can land on.
tabindex
An attribute: 0 puts an element in the Tab order, -1 makes it focusable only from code.
roving tabindex
A pattern where only one part of a widget has tabindex 0 and the arrow keys move it.
keyboard trap
A widget that keeps keyboard focus: a person cannot leave it with the keyboard.
focus ring
The outline the browser draws around the element that has keyboard focus.
slider
An ARIA role for something with a value between a minimum and a maximum, moved with the arrow keys.
aria-valuetext
A human-readable version of a slider's value, which a screen reader speaks instead of the number.
live region
An element whose text changes are spoken without moving focus.

Step by step

01Count the stops

The first example builds two lists of 8 stations: one with tabindex="0" on every row, one roving. It counts the Tab stops in each: 8 and 1.

02Move with the arrow keys

The roving list handles ArrowUp/Down/Left/Right, Home and End: it calls preventDefault(), moves the 0 to the new row and focuses it. Real KeyboardEvents drive it, and the Console logs where focus went.

03Leave the other keys alone

Tab is not handled: the Console reports default prevented: false, so the browser would move on to the next element. The chart is not a trap.

04Use a slider for a long line

In the second example a div with role="slider" stands for the whole rides line. Each arrow key changes aria-valuenow and aria-valuetext. A screen reader speaks the text.

05Announce what focus cannot say

The last lines of the second example call announce() twice with the same sentence. Between the calls the region is emptied, so the second one changes the text and is spoken again.

Try it yourself

  1. 1

    Start with no stop at all

    In the first example, change (i === 0 ? 0 : -1) to -1. The Console prints Tab stops, roving: 0. Nobody can Tab into the list, so nobody can use the arrow keys. A roving list always needs one 0.

  2. 2

    Jump to the end

    In the second example replace the first three press calls with press("End");. The Console prints valuenow: 3 | valuetext: Thu 5 Jun: 3,742 rides, the last day.

Code & diagrams

Roving tabindex: one Tab stop, arrow keys inside New tab

Focus moves for real (document.activeElement). The list stops at both ends instead of wrapping. The Tab press is not handled, so defaultPrevented stays false.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Tab stops, every row: 8
Tab stops, roving: 1
ArrowDown  -> focus: Quay        | default prevented: true | stops: 1
ArrowDown  -> focus: Clock Tower | default prevented: true | stops: 1
End        -> focus: Campus      | default prevented: true | stops: 1
ArrowDown  -> focus: Campus      | default prevented: true | stops: 1
Home       -> focus: Lantern     | default prevented: true | stops: 1
Tab        -> focus: Lantern     | default prevented: false | stops: 1
A day reader: a slider with words for its value New tab

Tab is left alone (prevented: false). announce() makes one polite live region (role=status) on first use and clears it before each message.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

ArrowLeft  -> valuenow: 2 | valuetext: Wed 4 Jun: 3,759 rides | prevented: true
ArrowLeft  -> valuenow: 1 | valuetext: Tue 3 Jun: 3,992 rides | prevented: true
Home       -> valuenow: 0 | valuetext: Mon 2 Jun: 2,979 rides | prevented: true
ArrowLeft  -> valuenow: 0 | valuetext: Mon 2 Jun: 2,979 rides | prevented: true
Tab        -> valuenow: 0 | valuetext: Mon 2 Jun: 2,979 rides | prevented: false
Region says: "Showing August." | role: status
Cleared first: ""
Region says again: "Showing August."

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Take every key

In the second example, delete the line else return; // every other key, Tab included, is left alone: no keyboard trap, so preventDefault() runs for every key.

terminal
$ Console, the last key
── what you'll see ──
Tab -> valuenow: 0 | valuetext: Mon 2 Jun: 2,979 rides | prevented: true

Break #2

Forget preventDefault for the arrows

In the first example, delete event.preventDefault(); from the keydown handler.

terminal
$ Console
── what you'll see ──
ArrowDown -> focus: Quay | default prevented: false | stops: 1

Myth vs fact

Myth

tabindex="0" on every bar makes the chart keyboard accessible.

Fact

It makes it keyboard reachable, and tiresome: 25 bars are 25 stops. A roving tabindex makes it one.

Myth

Screen reader users use the Tab key to read charts.

Fact

They mostly move with the screen reader's own keys. Keyboard focus matters for widgets and for people who see and use a keyboard only. Support both.

Myth

A tooltip on hover is enough.

Fact

Hover does not exist for the keyboard or for most touch screen readers. The same text must also come from focus.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    aria-valuetext is spoken on change, and aria-live regions speak on text change: do not do both for the same words, or a person hears each value twice. The dashboard uses valuetext for the day reader, and the live region only for changes that happen without focus (a new month).

  • ▸

    Browse mode and focus mode differ by product: in browse mode the screen reader may take arrow keys for its own reading. A role with a known keyboard pattern (slider, listbox, grid) generally makes it hand the keys to the page. Check the pattern in the ARIA Authoring Practices Guide, and test with NVDA and VoiceOver.

  • ▸

    The brush (Topic 8.5) is a drag, so WCAG 2.5.7 needs a way without dragging. The June, Summer and Whole year buttons from Topic 8.7 already meet it, and each is large enough for 2.5.8 (24 by 24 CSS pixels).

Remember this

  1. 1

    The problem this solves: hovering over a point shows its value to a mouse user. A keyboard user has nothing to hover with, and a screen reader user has no way to ask "what is this bar?". They need a path: reach the chart, step through its parts, hear each value. This is WCAG 2.1.1 Keyboard (level A): everything a mouse can do, the keyboard can do.

  2. 2

    One Tab stop, not one per mark. Giving every bar tabindex="0" puts 25 stops into the page, and a person who wants to skip the chart presses Tab 25 times. The first example counts: 8 rows with tabindex="0" are 8 stops. Use roving tabindex: only one part has tabindex="0" (the stop), the rest have -1 (focusable from code), and the arrow keys move focus and move the 0 with it.

  3. 3

    Which keys. Arrow keys step between neighbours. Home and End jump to the first and last. Page Up and Page Down jump a larger step (a week on the rides line). Escape closes a tooltip. Tab and Shift+Tab, and every key you do not use, must be left alone: call preventDefault() only for keys you handle. Swallowing Tab is a keyboard trap and fails WCAG 2.1.2 No Keyboard Trap (level A).

  4. 4

    Say each value aloud. Moving focus onto a bar whose accessible name is "Quay: 7,078 rides, number 2 of 25" makes a screen reader speak it. For a line with 365 points, one role="slider" element is a compact pattern: aria-valuemin, aria-valuemax, aria-valuenow (the position) and aria-valuetext (the words, "Tue 30 Dec 2025: 1,446 rides"). A screen reader speaks aria-valuetext whenever it changes. Screen readers differ in how they handle arrow keys in browse mode, and a widget role such as slider generally switches a screen reader into the mode that passes the keys to the page. Test it on a real one.

  5. 5

    Show where focus is. A focus ring must be visible (WCAG 2.4.7 Focus Visible, level AA) and must not be completely covered by sticky headers or tooltips (2.4.11 Focus Not Obscured (Minimum), level AA, new in WCAG 2.2). Use :focus-visible with a 3-pixel outline in the ink colour, which has contrast against the card in both themes. Mirror focus with the same highlight a hover gives: the dashboard dims other rows and shows the crosshair.

  6. 6

    Touch, switches and live regions. A touch screen reader (VoiceOver on iOS, TalkBack on Android) explores by swiping or touching, and a focusable chart still gets one swipe stop. Anything that changes without focus (a new month, a sorted table, a refreshed number) is spoken through a live region: an element with role="status" whose text is replaced (WCAG 4.1.3 Status Messages, level AA). Clear it first: the same sentence twice is only spoken if the text really changes. Two more rules from WCAG 2.2: a drag has a single-pointer alternative (2.5.7 Dragging Movements, AA: the brush has preset buttons) and small targets are at least 24 by 24 CSS pixels (2.5.8 Target Size (Minimum), AA).

Explain it without notes

01

What is a roving tabindex and why use it?

02

Which keys should a chart handle, and which must it leave alone?

03

Why is swallowing Tab a failure?

04

When do you use a live region instead of moving focus?

05

What is aria-valuetext for?

Practice

01

Make a three-item toolbar of buttons a roving tabindex. Which attribute changes on which keys?

02

Add Page Down to the first example, moving three rows at once. Where in the code?

Trade-offs

  • ↔

    A slider is compact for a long line and a screen reader says each day. It says nothing about the line's shape: the description and the table still do that work.

  • ↔

    A visible, strong focus ring is clear and can clash with the design. Use one outline colour from the theme tokens so it follows both themes.

Done when you can

  • I can make a chart a single Tab stop with a roving tabindex.

  • I handle arrows, Home, End and Escape, and leave Tab alone.

  • I give each focused part or slider position words a screen reader can speak.

  • I show a visible focus ring that works in both themes.

  • I use a live region for changes that focus does not carry.