Keyboard Navigation, Focus and tabindex
If it cannot be reached with the Tab key it does not work. Focus order, visible focus indicators, the three values of tabindex and the traps to avoid.
-
HTML Basics
- What is HTML: The Structure Layer of Every Web Page
- HTML Document Structure: DOCTYPE, html, head and body
- Elements, Tags and Attributes: The Vocabulary of HTML
- HTML Comments: Notes That Ship With Your Code
- Block Level and Inline Elements
- Writing and Running Your First HTML Page
- How a Browser Turns Markup Into a Page
- Text and Formatting
- Links and Navigation
- Images and Media
- Lists
- Tables
-
Forms
- Form Structure: form, action and method
- Input Types: Text, Email, Number, Date and the Rest
- Labels: The Most Important Element in a Form
- Checkboxes, Radio Buttons and Grouping
- select, option, optgroup and datalist
- textarea, File Uploads and Hidden Fields
- Buttons: submit, reset and button
- Built In Form Validation
- GET or POST: What Happens When a Form Is Submitted
- Semantic HTML
- HTML5 Features
- Head and Metadata
- HTML with CSS
- HTML with JavaScript
- Accessibility
-
HTML SEO
- How Google Works: Crawling, Indexing and Ranking
- SEO Friendly HTML Structure
- Titles and Descriptions That Earn Clicks
- Headings and Content Structure for Search
- Internal Linking and Anchor Text
- robots.txt and XML Sitemaps
- Canonical URLs and Duplicate Content
- Structured Data and JSON-LD
- Image SEO
- Core Web Vitals and Mobile Friendliness
- DevTools and Debugging
- Editor Productivity
- HTML Best Practices
- HTML Projects
- Advanced Projects
- Practice and Exams
Concept
A large number of people never use a mouse: screen reader users, people with motor impairments, people using switch devices or voice control, and anyone who simply works faster from the keyboard.
The test is short. Put the mouse away and use the page. Anything you cannot reach or activate is broken, whatever it looks like.
What is focusable by default
awith anhrefbuttoninput,select,textareasummaryiframe- Anything with
contenteditable - Anything with a
tabindex
Notice what is absent: div, span, p, li. A clickable div cannot be reached at all, which is the concrete reason it is a bug rather than a style preference.
Focus order
Focus follows DOM order, top to bottom. Not visual order.
/* the sidebar looks first and is still second in the tab order */
aside { order: -1; }Reordering visually with grid or flexbox splits the two apart, and a keyboard user then jumps around the page unpredictably. Write the markup in reading order and only reorder visually where the sequence genuinely does not matter.
tabindex
| Value | Effect |
|---|---|
tabindex="0" | Focusable, in normal DOM order |
tabindex="-1" | Focusable only from script, not by Tab |
tabindex="1" or higher | Jumps ahead of everything else. Avoid. |
tabindex="0"
<!-- a scroll container a keyboard must be able to scroll -->
<div class="table-scroll" tabindex="0" role="region" aria-label="Fee structure">
<table>...</table>
</div>Add it to something that genuinely needs to receive focus but is not natively focusable. Do not add it to make a div into a button - use a button.
tabindex="-1"
<main id="main-content" tabindex="-1">...</main>errorSummary.focus(); // works because it has tabindex="-1"The value for anything a script needs to move focus to: a skip link target, an error summary, the heading of a newly loaded view. It stays out of the tab order.
Positive values
<!-- do not do this -->
<input tabindex="1">
<input tabindex="2">A positive tabindex jumps ahead of every element with 0 or a default, which means adding one element with tabindex="1" reorders the whole page. It becomes unmaintainable within a few changes. Fix the DOM order instead.
Visible focus
The focus indicator is how a keyboard user knows where they are. Removing it makes a page unusable without a mouse.
/* never do this alone */
*:focus { outline: none; }
/* what to do instead */
:focus-visible {
outline: 3px solid #1e3a8a;
outline-offset: 2px;
border-radius: 2px;
}:focus-visible is the answer to the reason people remove outlines in the first place: it applies the ring for keyboard focus and not for a mouse click. The default browser outline is often removed because it appears on click; this keeps it exactly where it is needed.
The guidelines require a focus indicator with at least a 3 to 1 contrast ratio against the adjacent colour, and it must not be obscured by sticky headers or footers.
Skip links
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<header><nav><!-- thirty links --></nav></header>
<main id="main-content" tabindex="-1">...</main>
</body>.skip-link {
position: absolute;
left: -9999px;
top: 0;
background: #ffffff;
padding: 0.75rem 1rem;
z-index: 100;
}
.skip-link:focus {
left: 0;
}Move it off screen rather than using display: none, which would remove it from the tab order entirely and defeat the purpose.
Focus traps, good and bad
A deliberate trap keeps focus inside a modal dialog while it is open. The dialog element does this for you when opened with showModal().
An accidental trap is a place a keyboard cannot leave - a custom widget that captures Tab, or an embedded frame with no way out. The user has to reload the page. Test by tabbing all the way through and back.
Managing focus in a single page application
When a route changes without a page load, focus stays where it was and a screen reader announces nothing. The convention:
function afterNavigate(view) {
const heading = view.querySelector("h1");
heading.setAttribute("tabindex", "-1");
heading.focus();
document.title = `${view.dataset.title} - Riverside College`;
}Focus the new heading and update the document title. Without both, a screen reader user has no way to know the page changed.
Keyboard conventions to honour
| Key | Expected |
|---|---|
| Tab / Shift Tab | Move between focusable elements |
| Enter | Activate a link or a button |
| Space | Activate a button, toggle a checkbox, scroll the page |
| Arrow keys | Move within a group: radios, tabs, menus, sliders |
| Escape | Close a dialog, menu or popover |
| Home / End | Jump to the first or last item in a group |
Groups of related controls should be one tab stop with arrow keys inside, not one tab stop per item. Native radio groups already do this.
Important rules
- Focus follows DOM order, not visual order.
- Only positive tabindex values break the order;
0and-1do not. - Never remove a focus indicator without replacing it.
- Hidden elements must be truly hidden, or their controls stay in the tab order.
- Focus must be managed after any content change that replaces what was focused.
- A control that cannot be reached does not exist for a keyboard user.
Common mistakes
outline: nonewith nothing in its place.- Clickable
divelements. - Positive tabindex values.
- Menus hidden with
opacity: 0, leaving links focusable but invisible. - Focus lost after a dialog closes or a route changes.
- Custom widgets with no arrow key support.
- A focus ring hidden behind a sticky header.
Best practices
- Use native interactive elements and get keyboard support for free.
- Style
:focus-visibledeliberately, with strong contrast. - Add a skip link as the first focusable element.
- Use
tabindex="-1"for script focus targets,0sparingly, never a positive value. - Return focus to the trigger when a dialog closes.
- Tab through every page before shipping.
Practice
- Navigate one of your pages with the keyboard alone and list everything you cannot reach.
- Remove focus outlines, then add
:focus-visiblestyling and compare mouse and keyboard behaviour. - Add a skip link and confirm the next Tab press lands inside the main content.
- Add
tabindex="1"to one element and observe what happens to the whole page order.