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.

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

  • a with an href
  • button
  • input, select, textarea
  • summary
  • iframe
  • 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

ValueEffect
tabindex="0"Focusable, in normal DOM order
tabindex="-1"Focusable only from script, not by Tab
tabindex="1" or higherJumps 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.

<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

KeyExpected
Tab / Shift TabMove between focusable elements
EnterActivate a link or a button
SpaceActivate a button, toggle a checkbox, scroll the page
Arrow keysMove within a group: radios, tabs, menus, sliders
EscapeClose a dialog, menu or popover
Home / EndJump 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; 0 and -1 do 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: none with nothing in its place.
  • Clickable div elements.
  • 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-visible deliberately, with strong contrast.
  • Add a skip link as the first focusable element.
  • Use tabindex="-1" for script focus targets, 0 sparingly, never a positive value.
  • Return focus to the trigger when a dialog closes.
  • Tab through every page before shipping.

Practice

  1. Navigate one of your pages with the keyboard alone and list everything you cannot reach.
  2. Remove focus outlines, then add :focus-visible styling and compare mouse and keyboard behaviour.
  3. Add a skip link and confirm the next Tab press lands inside the main content.
  4. Add tabindex="1" to one element and observe what happens to the whole page order.

Useful resources

Hand picked references for this topic
Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All HTML notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.