Part IV β The Reach Β· Lesson 8.2
Keyboard Accessibility
In one lineEverything clickable must be reachable by Tab, activatable by Enter or Space, and visibly focused.
Where you've seen itA dialog that opens fine, then leaves Tab cycling through the page behind it while Escape does nothing.
Diagram
TAB ORDER FOLLOWS THE DOM, NOT THE CSS
βββββββββββββββββββββββββββββββββββββββββββββββ
β [1 Skip to content] β
β ββββββββββββ βββββββββββββββββββββββββ β
β β [2 Logo] β β [3 Search] [4 Sign in]β β
β ββββββββββββ βββββββββββββββββββββββββ β
β βββββββββββββββββββββββ βββββββββββββββββ β
β β main β β sidebar β β
β β [5 Filter] β β [7 Related] β β
β β [6 Buy now] β β β β
β βββββββββββββββββββββββ βββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββ
If CSS (order, flex-direction: row-reverse, grid placement)
moves things visually, the tab order no longer matches what
the user sees. Fix the DOM, not the tabindex.THE KEY CONTRACT β what users expect, per widget
button Enter, Space activate
link Enter navigate
checkbox Space toggle
radio group Arrows move + select (one tab stop)
select / menu Arrows, Home, End move Β· Enter select Β· Esc close
dialog Esc close, focus returns to trigger
tabs Arrows switch Β· Tab moves OUT of the set
tree/grid Arrows two-dimensional movement
Build a custom widget and you inherit ALL of its expected keys.tabindex β three values, two of them fine
0 in the natural tab order β custom interactive elements
-1 focusable by script only β dialogs, error summaries,
roving-tabindex items
>0 jumps the queue β never β breaks the whole pageHow it works
- Use
<button>and<a href>. They're focusable, activatable, and announced correctly with zero code. - If you must use a
div, you owe itrole,tabindex="0", and bothkeydownhandlers β Enter and Space for buttons. - Never remove focus outlines. Restyle them instead;
:focus-visibleshows a ring for keyboard users without showing it on mouse clicks. - Add a skip link as the first focusable element so keyboard users can bypass the nav.
- Use roving tabindex for composite widgets (toolbars, radio groups, tabs): one tab stop for the group, arrows to move within it.
/* keep the ring for keyboard users; hide it for mouse clicks */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }Trade-offs
| Do | Don't |
|---|---|
<button onClick> | <div onClick> |
:focus-visible restyle | outline: none |
| Roving tabindex for groups | 12 tab stops in a toolbar |
| DOM order = visual order | tabindex="5" to patch order |
Interview angle
"How do you test keyboard accessibility?"
- Put the mouse away and Tab through the whole flow: everything interactive must be reachable, activatable, and visibly focused.
- Check the order matches the visual layout, since CSS can reorder without changing the DOM.
- For each custom widget, verify the expected keys β Escape on dialogs and arrows on composite widgets are the usual gaps.
Recap
- Reachable, activatable, visible β the three keyboard requirements.
- Tab order comes from the DOM; positive
tabindexis always the wrong fix. - Custom widgets inherit the full key contract of the thing they imitate.