Many people never use a mouse: keyboard-only users, screen reader users, people with motor difficulties, power users. If a feature only works with a mouse, it does not work for them. JavaScript is often where accessibility gets lost, so build it in from the start.
The golden rule: use the right element first
Native elements come with keyboard support for free.
| Need | Use | You get for free |
|---|---|---|
| An action | <button> | Tab focus, Enter and Space activate it |
| Navigation | <a href> | Tab focus, Enter follows it |
| A choice | <input type="checkbox">, radio, <select> | Space, arrows, labels |
| A popup | <dialog> | focus handling, Escape to close |
| Expandable text | <details> | Enter / Space toggle |
A <div onclick> has none of this. It cannot be focused, and Enter does nothing.
<style>
.fake { display: inline-block; padding: 6px 12px; background: #ddd; cursor: pointer; margin-right: 8px; }
</style>
<div class="fake" id="fake">Fake button (div)</div>
<button id="real">Real button</button>
<p id="out">Press Tab, then Enter, in the result.</p>
<script>
const out = document.querySelector("#out");
document.querySelector("#fake").addEventListener("click", () => { out.textContent = "div clicked (mouse only)"; });
document.querySelector("#real").addEventListener("click", () => { out.textContent = "button activated (mouse or keyboard)"; });
</script>
Try tabbing: the real button gets focus and works with Enter and Space. The div is skipped completely.
Keys users expect
| Key | Expected behaviour |
|---|---|
| Tab / Shift+Tab | move to the next / previous interactive element |
| Enter | activate a link or button, submit a form |
| Space | activate a button, toggle a checkbox, scroll the page |
| Escape | close a dialog, menu or popup, cancel |
| Arrow keys | move within a widget: tabs, menus, radio groups, sliders, lists |
| Home / End | first / last item in a list or menu |
Arrow-key navigation (roving tabindex)
In a group like a toolbar or tab list, only one item is in the Tab order. Arrow keys move between items.
<div role="toolbar" aria-label="Text formatting" id="bar">
<button tabindex="0">Bold</button>
<button tabindex="-1">Italic</button>
<button tabindex="-1">Underline</button>
</div>
<script>
const buttons = [...document.querySelectorAll("#bar button")];
document.querySelector("#bar").addEventListener("keydown", event => {
const index = buttons.indexOf(document.activeElement);
if (index === -1) return;
let next = index;
if (event.key === "ArrowRight") next = (index + 1) % buttons.length;
else if (event.key === "ArrowLeft") next = (index - 1 + buttons.length) % buttons.length;
else if (event.key === "Home") next = 0;
else if (event.key === "End") next = buttons.length - 1;
else return;
event.preventDefault();
buttons[index].tabIndex = -1;
buttons[next].tabIndex = 0;
buttons[next].focus();
});
</script>
tabindex
| Value | Effect |
|---|---|
tabindex="0" | makes an element focusable, in the natural order |
tabindex="-1" | focusable by script (el.focus()), but not by Tab |
tabindex="1" or higher | avoid: it breaks the natural order |
Custom controls must be completed
If you really must make a custom control (rare), you need all of: a role, tabindex="0", keyboard handlers for Enter and Space, a visible focus style and proper ARIA state. Compare that to <button>: it is almost always better to use the real element and style it.
Test it
Unplug the mouse (or just stop using it). Can you reach and use every control, see where focus is, and escape from every popup? If not, fix that first.
Do not remove focus outlines
outline: none without a replacement leaves keyboard users lost. See Focus Styles in the CSS course.