Français

Put down the mouse: everything about keyboard navigation

Table of contents

Five seconds to understand

Try it. Right now. Put down your mouse, open a new tab on your site, and press Tab.

Maybe everything works, the focus is visible, the order is logical, every interactive element is reachable. If so, well done, that's far from the norm. But maybe you can't see where you are, the focus disappeared somewhere in the CSS. Or you're navigating from one element to the next in an order that has nothing to do with what you're reading on screen. Or some buttons or links are simply unreachable.

Whatever the result, you just experienced in 5 seconds what millions of people live through on every visit. The difference is they don't have the choice to pick up the mouse again.

This article will show you who these people are, why so many sites ignore them, and how to build keyboard navigation that doesn't leave them in the dark.

For whom the keyboard isn't a choice

Michael J. Fox (Marty McFly in Back to the Future) hasn't been able to drive for years. The tremors caused by Parkinson's disease also make using a mouse extremely difficult. Clicking precisely on a button, holding the cursor on an element, double-clicking: mundane gestures that become real obstacles when your hands no longer respond as you'd like.

For these people, keyboard navigation isn't a preference. It's the only option.

And Michael J. Fox is far from alone. Parkinson's disease affects more than 10 million people worldwide. But it's just one condition among many. Musculoskeletal disorders, stroke aftereffects, partial paralysis, amputations affect millions more who also depend on functional keyboard navigation.

And then there are temporary situations. A broken arm. A sprained wrist. Tendinitis after an intensive week of coding. A sleeping baby in one arm while the other navigates. Disability isn't always permanent, but inaccessibility is constant.

If your interface doesn't work with the keyboard, you're excluding these people. Silently, without knowing. And just like with colors that lie, 56% of unhappy users say nothing, they leave.

The invisible focus

There's a line of CSS you've probably seen, or written:

reset.css
*:focus {
  outline: none;
}

This line removes the only visual indicator a keyboard user has to know where they are on the page. It's the equivalent of removing house numbers from an entire neighborhood: technically, the houses are still there, but no one can find them.

Why do so many projects include it? Because the browser's default outline, that blue or dotted rectangle, also appears on mouse click. Designers find it ugly, developers remove it, and no one notices the problem because the team uses a mouse.

The solution has existed for years and fits in one word: :focus-visible. This pseudo-selector only applies the focus style when the browser determines the user would benefit from a visible indicator, that is, during keyboard navigation, not on mouse click.

focus.css
/* Remove default outline only when
   focus-visible does not apply */
:focus:not(:focus-visible) {
  outline: none;
}
 
/* The real focus style — visible, robust, adaptive */
:focus-visible {
  outline: var(--outline-width) solid var(--color-primary);
  outline-offset: var(--outline-offset);
  box-shadow:
    0 0 0 var(--outline-offset) var(--color-background),
    0 0 0 var(--outline-spread) var(--color-primary);
}
 
@media (forced-colors: active) {
  :focus-visible {
    outline: var(--outline-width) solid Highlight;
    outline-offset: var(--outline-offset);
    box-shadow: none;
  }
}
 
@media (prefers-reduced-motion: no-preference) {
  :focus-visible {
    transition: outline-offset var(--duration-fast) ease;
  }
}

This pattern, the double ring, creates a visible outline on all backgrounds: the first shadow in background color isolates the outline, the second in primary color makes it visible regardless of the backdrop. Express the thicknesses (the --outline-width custom properties and friends) in em or rem so the focus adapts to the element's font size, and therefore to zoom. And the forced-colors block ensures focus remains visible when the user imposes their own theme. Box-shadow disappears in high contrast mode, hence the outline alone with the system keyword Highlight.

WCAG 2.2 criterion 2.4.13 (Focus Appearance) requires the indicator to cover at least the area of a 2px perimeter around the component, with a 3:1 contrast ratio between the focused and unfocused states. The double ring you just saw exceeds these requirements.

The order that lies

Focus follows the DOM, not the CSS. It's a simple sentence, but its consequences are everywhere.

When you use flexbox with order, row-reverse, or column-reverse, you rearrange what the user sees, but not what the keyboard traverses. Grid can produce the same effect when grid-area or grid-template-areas place elements in a visual order different from the DOM, but it's not systematic. The mouse user sees a logical interface. The keyboard user may navigate in an order that bears no relation to what they see on screen.

order-mismatch.html
<nav class="nav-main">
  <a href="/about">About</a>
  <a href="/blog">Blog</a>
  <a href="/contact">Contact</a>
</nav>
order-mismatch.css
.nav-main {
  display: flex;
}
 
.nav-main a:nth-child(1) {
  order: 3;
}
 
.nav-main a:nth-child(2) {
  order: 1;
}
 
.nav-main a:nth-child(3) {
  order: 2;
}

Visually: Blog → Contact → About. By keyboard: About → Blog → Contact. Focus jumps all over the place, and the user doesn't understand why.

The solution isn't tabindex. At least, not with positive values. tabindex="1", tabindex="2", tabindex="3", it seems logical, but it creates a parallel priority system that conflicts with the natural DOM order. Every element with a positive tabindex passes before all other elements on the page. Add a new component and the entire order needs reworking.

tabindex-dont.html
<!-- Don't do this -->
<button tabindex="3">Submit</button>
<button tabindex="1">Cancel</button>
<button tabindex="2">Draft</button>

The real solution is simpler: make the HTML order match the visual order. If the design requires a different display order, change the HTML, not the CSS order. And if you need to make a non-natively focusable element focusable, the first question to ask yourself is: why isn't this element a <button> or an <a>? Native interactive HTML elements are focusable by default, correctly announced by screen readers, and keyboard-activatable without extra effort. tabindex="0" exists, but it's a last resort, not a first reflex.

How to verify the order is consistent? Accessibility Insights for Web offers a "Tab Stops" feature that displays the exact focus order on your page, numbered, overlaid directly on the content. In seconds, you can see if the numbers follow the visual logic or jump around.

Keyboard traps

There's something worse than invisible focus or inconsistent order: focus that enters somewhere and can't get out.

It's called a keyboard trap. You navigate with Tab, arrive in a component, and whatever you do, Tab brings you back to the same place. Escape does nothing. The only way out: reload the page.

The usual culprits: poorly implemented modals, custom video players, rich text editors, date widgets, dropdown menus that capture focus without releasing it.

The paradox is that in some cases, confining focus is exactly the right behavior. When a modal opens, focus must stay inside, otherwise the keyboard user can interact with content behind the modal, which is both confusing and potentially dangerous (submitting a form they can't see, for example).

The difference between a focus trap (intentional) and a keyboard trap (bug) comes down to three rules:

  1. The user can exit : Escape closes the modal and returns focus to where it was before opening
  2. Focus is announced : the screen reader knows a modal is open and knows its content
  3. The rest of the page is disabled : not just visually, but for keyboard and screen readers too

It's this third point that was historically problematic. Disabling the rest of the page required adding aria-hidden="true" and tabindex="-1" on all elements outside the modal. Fragile, verbose, bug-prone.

The native <dialog> element, opened with .showModal(), solves all of this without any external library:

dialog.html
<button id="open-confirm" type="button">Delete</button>
 
<dialog id="confirm">
  <form method="dialog">
    <h2>Confirm deletion</h2>
    <p>This action is irreversible.</p>
    <button type="submit" value="cancel" autofocus>Cancel</button>
    <button type="submit" value="confirm">Confirm deletion</button>
  </form>
</dialog>
dialog.js
const dialog = document.getElementById('confirm');
 
document.getElementById('open-confirm').addEventListener('click', () => {
  dialog.showModal();
});
 
dialog.addEventListener('close', () => {
  if (dialog.returnValue === 'confirm') {
    // deletion action
  }
});
 
dialog.addEventListener('click', (e) => {
  if (e.target === dialog) dialog.close();
});

One detail: a click in the <dialog>'s own padding also has e.target === dialog and would close the modal. For only the backdrop to trigger the close, wrap the content in a container that fills the whole modal surface (padding: 0 on the <dialog>, the padding on the container).

.showModal() does the work: focus is automatically confined inside the modal, the rest of the page becomes inert, and Escape closes the modal. Focus should return to the trigger element on close, but behavior varies across browsers. In practice, it's safer to manage this return explicitly in JavaScript. No manual aria-hidden, no tabindex="-1" on every outside element, no scroll lock in JavaScript.

dialog.css
dialog::backdrop {
  background-color: light-dark(
    hsl(0 0% 0% / var(--backdrop-opacity)),
    hsl(0 0% 0% / var(--backdrop-opacity-dark))
  );
}

No z-index to manage, no scroll lock in JavaScript, no aria-modal. The browser does the work.

Building solid keyboard navigation

The previous sections showed you the most common problems: invisible focus, inconsistent order, keyboard traps. Here are the foundations to avoid reproducing them.

On most sites, the first Tab brings focus to the logo, the second to the first navigation link, the third to the second link, and so on. A keyboard user must traverse the entire navigation before reaching the content, on every page.

The skip link solves this: a link invisible at rest, which appears on the first Tab and lets you jump directly to the main content.

skip-link.html
<body>
  <a href="#main-content" class="skip-link">
    Skip to main content
  </a>
  <header><!-- navigation links --></header>
  <main id="main-content">
    <!-- content -->
  </main>
</body>
skip-link.css
.skip-link {
  position: absolute;
  inset-inline-start: -100vw;
  background: var(--color-primary);
  color: var(--color-primary-foreground);
  padding: var(--space-2) var(--space-4);
  text-decoration: none;
  z-index: 1000;
}
 
.skip-link:focus-visible {
  inset-inline-start: var(--space-2);
  inset-block-start: var(--space-2);
}

No display: none, that would remove the link from the accessibility tree. We position it off-screen and bring it back on focus-visible. Logical properties (inset-inline-start) for RTL compatibility.

The 5 keys to always support

Every interactive component must respond to a minimum of 5 keys:

  • Tab : move to the next focusable element
  • Shift + Tab : go back to the previous element
  • Enter : activate the element (link, button)
  • Space : also activate buttons, checkboxes and switches. A custom button that only responds to Enter is broken
  • Escape : close, cancel, exit the current context

Native HTML elements (<a>, <button>, <input>, <select>) handle Tab, Enter and Space by default. Escape, however, is only native in a few contexts (a <dialog> opened with showModal(), an expanded <select>): as soon as your component opens a context, it's your JavaScript that has to listen for it. That's one more reason to use semantic HTML rather than <div> elements with manual event handlers.

Focus after a route change

In a SPA, changing pages doesn't reload the document, focus stays where it was. The keyboard user doesn't know the content has changed. The screen reader doesn't either.

Two approaches:

  • Send focus to the <h1> of the new page: the user immediately knows where they are
  • Send focus to the main container (<main>): the user can start navigating the content

In both cases, the target element must have tabindex="-1" to be programmatically focusable without entering the natural tab order.

The overall criteria

WCAG covers keyboard navigation through several criteria:

The ADA Title II rule requires WCAG 2.1 AA, which includes all Level A and AA criteria above. And with 3,117 federal web accessibility lawsuits filed in 2025 alone, courts are applying the same standard to private websites.

All these criteria converge on the same idea: the keyboard user must know where they are, know where they're going, and be able to get out.

And to verify all of this in one pass, the most effective test remains the simplest: put down your mouse, and navigate your site with Tab. If you can accomplish every action, in a logical order, without losing focus and without getting stuck, your site meets the essentials.

Sources