In the previous lesson we built widgets and, time and again, we pointed "to 05-02" for the keyboard and focus mechanics. The moment has arrived. The keyboard is the base layer of all interactive accessibility: if something works with the keyboard, it almost always works with a screen reader, a switch, or voice control too. Lucía navigates exclusively with the keyboard because of her reduced mobility; Marta uses NVDA, which relies on the browser's focus model. If your component is not keyboard operable, for them it simply does not exist. In this lesson we cover the fundamentals: what is focusable, how tabindex works, how to move focus without stealing it, how to avoid keyboard traps, and two key techniques —roving tabindex and focus trap— plus the reference table of keys that the rest of the course cites.
Contents
- Which elements are focusable natively
tabindex: 0, -1, and why never positive- Focus order is DOM order
- Visible focus and programmatic focus (
element.focus()) - Keyboard traps (criterion 2.1.2) and how to avoid them
- Skip links that really work
- Technique: roving tabindex
- Technique: focus trap for modal dialogs
- Exhaustive table of key conventions
- Common mistakes, exercises, and conclusion
- Which elements are focusable natively
Only a small set of HTML elements can receive keyboard focus unaided:
| Element | Focusable by default | Note |
|---|---|---|
<a href="…"> |
Yes | Only if it has href |
<button> |
Yes | Also responds to Enter/Space |
<input>, <select>, <textarea> |
Yes | Except when disabled |
<div>, <span>, <p> |
No | Need tabindex |
Element with tabindex="0" |
Yes | Added manually |
Element with contenteditable |
Yes | — |
The lesson here is blunt: use native elements. A <button> is focusable, operable with Enter and Space, announced as a button, and compatible with every assistive technology without writing a line of JavaScript. A <div role="button">, by contrast, forces you to add tabindex="0", handle Enter and Space by hand, and even then you risk incompatibilities. The first rule of ARIA from Module 4 —"prefer native HTML"— is, at heart, a keyboard rule.
tabindex: 0, -1, and why never positive
tabindex: 0, -1, and why never positivetabindex controls whether an element enters the tab order and its position. It has three uses, and only two of them are good:
<!-- tabindex="0": enters the tab order in its natural DOM position -->
<div role="button" tabindex="0">Custom button</div>
<!-- tabindex="-1": does NOT enter the tab order, but IS focusable via JS -->
<div id="panel" tabindex="-1">Panel I move focus to with .focus()</div>
<!-- tabindex="5": NEVER. It breaks the natural order. -->
<input tabindex="5">tabindex="0": makes an element focusable that isn't, respecting DOM order. Use it for custom widgets.tabindex="-1": removes the element from theTabsequence, but allows focusing it programmatically withelement.focus(). It is essential for moving focus to a panel, to a heading after navigating, or for the roving tabindex.- Positive
tabindex(1, 2, 3…): never. A positive value jumps ahead of the entire natural order, creating an unpredictable focus path that is impossible to maintain. A single straytabindex="1"is enough to disorder the whole page. If you need to change the order, reorder the HTML.
- Focus order is DOM order
When the user presses Tab, focus advances following the order of the HTML code, not the visual order on screen. This has an enormous practical consequence: if you use CSS (flexbox order, grid, position) to visually reorder the elements, focus will follow DOM order and will not match what is seen.
/* DANGER: the second element appears first, but focus visits it later */
.bar { display: flex; }
.bar .primary-action { order: -1; } /* jumps to the start visually */Lucía will see the primary button on the left, but when she tabs, focus will go somewhere else first. WCAG criterion 2.4.3 (Focus Order) requires the order to be logical and meaningful. The safe rule: keep the DOM order consistent with the visual reading order and use CSS for appearance, not to reorder the interaction flow.
- Visible focus and programmatic focus
Two sides of the same coin:
Visible focus. The keyboard user must see where focus is at all times (criterion 2.4.7). The web's most serious historical mistake is outline: none with no replacement. As we saw in Module 4, the modern solution is :focus-visible:
/* Correct: a clear focus ring, only when navigating with the keyboard */
:focus-visible {
outline: 3px solid #1a73e8;
outline-offset: 2px;
}
/* NEVER leave this without a visible alternative: */
/* button:focus { outline: none; } ❌ */Programmatic focus. With element.focus() we move focus from JavaScript. It is the tool every widget in this module uses. Golden rules:
// Correct: move focus as the RESULT of a user action
openBtn.addEventListener('click', () => {
modal.hidden = false;
modal.querySelector('input').focus(); // the user asked to open → we move focus
});- Never steal focus without the user asking for it. Moving focus during scroll, in a
setInterval, or when an ad loads is disorienting and can violate criterion 3.2.1 (On Focus) and 3.2.5. Focus moves as a response to an action, not by surprise. - To focus a non-interactive container (a panel, an
<h2>for a new section), give ittabindex="-1"first, or.focus()will have no effect.
- Keyboard traps (criterion 2.1.2) and how to avoid them
A keyboard trap occurs when focus enters a component and cannot leave with the keyboard. The user is trapped and, without a mouse, has no way out. Criterion 2.1.2 (No Keyboard Trap) is level A: it is unacceptable. Typical causes:
- A third-party widget (a rich text editor, a map) that captures
Taband never releases it. - A keyboard handler that calls
event.preventDefault()onTabwithout forwarding focus elsewhere.
// TRAP: Tab is canceled but focus is not moved anywhere → trapped
editor.addEventListener('keydown', (e) => {
if (e.key === 'Tab') e.preventDefault(); // ❌ the user cannot get out
});Note: a modal focus trap is different and is legitimate, because the user can always leave with Esc (we cover this in point 8). The difference between a forbidden trap and a correct trap is that the trap always offers a standard escape route.
- Skip links that really work
A skip link ("Skip to main content") lets Lucía and Marta avoid tabbing through the entire header and menu on every page. It is an anchor that must actually move focus, not just scroll:
<a href="#main" class="skip-link">Skip to main content</a>
<!-- … header and navigation … -->
<main id="main" tabindex="-1">…</main>/* Hidden until it receives focus: appears on tab */
.skip-link {
position: absolute;
left: -9999px;
}
.skip-link:focus {
left: 1rem;
top: 1rem;
}The critical detail is the tabindex="-1" on the <main>: without it, in some browsers the link scrolls but focus stays at the top, and the next Tab press goes back to the menu. With tabindex="-1", activating the link lands focus inside <main> and navigation continues from there.
- Technique: roving tabindex
In a group of related elements (tabs, toolbar, custom radio group) we do not want Tab to visit each one: it would be exhausting. The APG specifies that Tab enters the group with a single stop and the arrow keys move within it. That is implemented with roving tabindex: at all times, only one element in the group has tabindex="0" and the rest have tabindex="-1"; as you move with the arrow keys, we "roll" that 0.
const items = [...document.querySelectorAll('[role="tab"]')];
let index = 0;
function move(newIndex) {
items[index].tabIndex = -1; // the previous one leaves the tab order
index = (newIndex + items.length) % items.length; // wraps around
items[index].tabIndex = 0; // the new one enters the tab order
items[index].focus(); // and receives focus
}
items.forEach((item, i) => {
item.tabIndex = i === 0 ? 0 : -1; // initial state
item.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') { move(index + 1); e.preventDefault(); }
if (e.key === 'ArrowLeft') { move(index - 1); e.preventDefault(); }
if (e.key === 'Home') { move(0); e.preventDefault(); }
if (e.key === 'End') { move(items.length - 1); e.preventDefault(); }
});
});This way, Lucía presses Tab once to enter the tabs of the Cursalia dashboard and then uses the arrow keys to move through them, exactly as a screen reader user expects.
- Technique: focus trap for modal dialogs
When enrollment (the modal from 05-01) is open, focus must cycle inside the dialog: on reaching the last element and pressing Tab, we go back to the first; with Shift+Tab on the first, we jump to the last. And Esc closes it. Full implementation:
function activateTrap(modal, trigger) {
// All the focusable elements INSIDE the modal
const focusables = modal.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusables[0];
const last = focusables[focusables.length - 1];
first.focus(); // initial focus inside the dialog
function onKeyDown(e) {
if (e.key === 'Escape') {
close();
return;
}
if (e.key !== 'Tab') return;
// Focus cycling: here is the "trap"
if (e.shiftKey && document.activeElement === first) {
last.focus(); // Shift+Tab on the first → to the last
e.preventDefault();
} else if (!e.shiftKey && document.activeElement === last) {
first.focus(); // Tab on the last → to the first
e.preventDefault();
}
}
function close() {
modal.hidden = true;
modal.removeEventListener('keydown', onKeyDown);
trigger.focus(); // RETURN focus to whoever opened it (essential)
}
modal.addEventListener('keydown', onKeyDown);
return close;
}This trap is correct because Esc always offers a way out: it does not violate 2.1.2. Modern note: the native <dialog> element with showModal() implements much of this for you (trapped focus and Esc), so prefer it when you can and reserve this code for browsers or cases that require it.
- Exhaustive table of key conventions
This is the reference table that 05-01 and Module 7 cite. It follows the APG conventions by widget type:
| Widget | Key(s) | Expected action |
|---|---|---|
| Button | Enter, Space |
Activate |
| Link | Enter |
Follow the link (Space does NOT activate it; it scrolls) |
| Checkbox | Space |
Check/uncheck |
| Radio (group) | ↑ ↓ ← → |
Move and select within the group; Tab enters/leaves the group |
| Menu / menu button | Enter/Space/↓ opens; ↑ ↓ moves; Esc closes; Home/End ends |
Navigate options |
| Tabs | ← → (or ↑ ↓ if vertical) moves; Home/End first/last; Tab moves to the panel |
Switch tabs |
| Combobox / listbox | ↓/↑ opens and moves; Enter selects; Esc closes; typing filters |
Choose a filter option |
| Accordion / disclosure | Enter, Space |
Expand/collapse the section |
| Modal dialog | Esc closes; Tab/Shift+Tab cycle inside (focus trap) |
Operate and close |
| Slider | ← → / ↑ ↓ step; Home/End min/max; PageUp/PageDown large jump |
Adjust value |
Mnemonic rule: Enter/Space activate, Esc cancels/closes, arrow keys navigate within a group, Home/End go to the extremes. If you respect these conventions, Marta and Lucía feel at home because they match those of desktop applications.
Common Mistakes and Tips
onclickon a<div>without keyboard support. The click works with a mouse but Lucía cannot activate it. Use a<button>or, if there is no other choice, addtabindex="0"and handleEnterandSpace.- Positive
tabindex. It breaks the focus order of the whole page. Forbidden. outline: nonewith no alternative. It leaves keyboard users unable to tell where they are. Use:focus-visible.- Canceling
Tabwithout moving focus. It creates a trap (2.1.2). If you interceptTab, always forward focus somewhere. - Stealing focus. Moving focus without the user asking (on scroll, timers, ad loads) is disorienting and violates 3.2.x.
- Tip: test every page by unplugging the mouse. If you can reach everything, activate everything, and always see where you are, you have won half the accessibility battle.
Exercises
Exercise 1. Explain the difference between tabindex="0" and tabindex="-1" and give a real use case for each.
Exercise 2. The following skip link scrolls but focus returns to the menu on the next Tab press. What is missing?
Exercise 3. You have a toolbar with five buttons and you want Tab to enter with a single stop and the arrow keys to move between them. Which technique do you use and what is its core idea in one sentence?
Solutions
Solution 1. tabindex="0" inserts a non-focusable element into the natural tab order (case: a custom <div role="button"> that must be reachable with Tab). tabindex="-1" removes it from the Tab order but allows focusing it via JavaScript with .focus() (case: a panel or a <main> you move focus to after an action, or the inactive elements of a roving tabindex).
Solution 2. tabindex="-1" is missing on the <main>. Without it, the anchor moves the scroll but not the focus, so focus stays in the header and Tab goes back to the menu. With <main id="main" tabindex="-1">, activating the link lands focus in the main content.
Solution 3. You use roving tabindex: only the "active" button has tabindex="0" and the others have tabindex="-1"; pressing the arrow keys moves that 0 (and focus) to the next button. Core idea in one sentence: a single tab stop for the whole group; the arrow keys move focus within it.
Conclusion
You now master the base layer: what is focusable, how tabindex behaves, why focus order follows the DOM, how to move focus without stealing it, how to avoid keyboard traps, and the two techniques that hold up the module's widgets —roving tabindex and focus trap—, plus the reference table of keys. With this, the components from 05-01 become fully operable without a mouse. We have made the interaction accessible; we still need to make the multimedia content accessible. In the next lesson, 05-03: Accessible Video and Audio Content, we will implement captions, transcripts, and audio description on the Cursalia video-lesson player, for Diego, Marta, and Hugo.
Web Accessibility Course
Module 1: Introduction to Web Accessibility
- What Is Web Accessibility?
- The Importance of Web Accessibility
- Overview of Accessibility Laws and Standards
- Introduction to WCAG
Module 2: Understanding Disabilities and Assistive Technologies
Module 3: Principles of Accessible Design
- Perceivable: Making Content Available to the Senses
- Operable: User Interface and Navigation
- Understandable: Information and Operation
- Robust: Compatibility with Current and Future Technologies
Module 4: Implementing Accessibility in HTML and CSS
Module 5: Accessibility in JavaScript and Multimedia
- Creating Accessible JavaScript Widgets
- Keyboard Accessibility
- Accessible Video and Audio Content
- Providing Text Alternatives for Images
Module 6: Accessibility Testing and Evaluation
- Manual Testing Techniques
- Automated Testing Tools
- User Testing with Assistive Technologies
- Interpreting Accessibility Reports
