Keyboard Navigation and Focus Management

โ–ถ Try It Yourself

Keyboard Navigation and Focus Management

1. Introduction

Keyboard accessibility is the foundation of all assistive technology support โ€” screen readers, switch devices, mouth sticks, and head trackers all rely on the browser’s keyboard navigation model. This lesson covers how the tab order is established, how to control focus programmatically with tabindex, and how to build keyboard-accessible custom components that follow established interaction patterns from the ARIA Authoring Practices Guide.

2. Concept

Focus and Tab Order Behaviour

Element / Setting Focusable by Default? tabindex Needed?
Links, buttons, inputs, select, textarea Yes No
Custom widgets (div, span) No tabindex=”0″ to add to tab order
Programmatic focus target (modal, skip destination) No tabindex=”-1″ โ€” focusable via JS only
tabindex > 0 Yes โ€” focused first Avoid โ€” breaks natural DOM flow
hidden attribute, display:none Removed from tab order N/A
Note: Never use tabindex values greater than 0. A tabindex="5" element is focused before any tabindex="0" or naturally focusable elements, creating a confusing and nearly impossible-to-maintain tab order.
Tip: When a dialog or modal opens, move focus to the first interactive element inside it (or to the dialog container). When the dialog closes, return focus to the element that triggered it. This is the expected pattern across all assistive technologies.
Warning: CSS pointer-events: none hides elements from mouse interaction but does not remove them from the keyboard tab order. Use display: none, visibility: hidden, or the hidden attribute to remove elements from both mouse and keyboard interaction.

3. Basic Example

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Focus Management Demo</title>
    <style>
      :focus-visible { outline: 3px solid #0070f3; outline-offset: 2px; }
      [hidden] { display: none; }
    </style>
  </head>
  <body>

    <!-- tabindex="0": adds custom element to natural tab order -->
    <div
      role="button"
      tabindex="0"
      onclick="alert('Activated')"
      onkeydown="if(event.key==='Enter'||event.key===' '){alert('Activated');}"
    >
      Custom Widget (tabindex="0")
    </div>

    <!-- tabindex="-1": focusable by JS, not in Tab sequence -->
    <div
      id="modal"
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
      tabindex="-1"
      hidden
    >
      <h2 id="modal-title">Confirm Deletion</h2>
      <p>This action cannot be undone.</p>
      <button type="button" id="confirm-btn">Delete</button>
      <button type="button" id="cancel-btn">Cancel</button>
    </div>

    <button type="button" id="open-modal">Delete Item</button>

    <script>
      var openBtn = document.getElementById('open-modal');
      var modal   = document.getElementById('modal');
      var confirmBtn = document.getElementById('confirm-btn');
      var cancelBtn  = document.getElementById('cancel-btn');

      openBtn.addEventListener('click', function() {
        modal.hidden = false;
        confirmBtn.focus();          // move focus into dialog
      });

      function closeModal() {
        modal.hidden = true;
        openBtn.focus();             // return focus to trigger
      }

      cancelBtn.addEventListener('click', closeModal);
      confirmBtn.addEventListener('click', function() {
        closeModal();
      });
      modal.addEventListener('keydown', function(e) {
        if (e.key === 'Escape') closeModal();
      });
    </script>

  </body>
</html>

4. How It Works

Step 1 โ€” Natural Tab Order

Tab moves focus through interactive elements in DOM order. Structure your HTML so DOM order matches the visual reading order โ€” never rely on CSS positioning alone to reorder content that is out-of-sequence in the HTML.

Step 2 โ€” tabindex=”0″

Adds a non-interactive element into the tab order at its natural DOM position. You must also add keyboard event handlers (Enter and Space keys) to make it operable. Because this is complex, always prefer a native <button> or <a> where possible.

Step 3 โ€” tabindex=”-1″

Makes an element focusable via element.focus() in JavaScript without adding it to the natural Tab sequence. Use for modal containers, skip link destinations, or the active item in a composite widget (like a tab panel or listbox).

Step 4 โ€” Focus Trap in Modals

When a modal is open, focus must cycle within it. Intercept Tab at the last focusable element (cycle back to first) and Shift+Tab at the first (cycle to last). This is WCAG 2.1.2 โ€” the trap must be intentional and escapable via the Escape key.

5. Real-World Example

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Accessible Tab Component</title>
    <style>
      [role="tabpanel"][hidden] { display: none; }
      [role="tab"][aria-selected="true"] {
        border-bottom: 3px solid #0070f3;
        font-weight: bold;
      }
      :focus-visible { outline: 3px solid #0070f3; outline-offset: 2px; }
    </style>
  </head>
  <body>
    <div role="tablist" aria-label="Course chapters">
      <button role="tab" id="tab-1" aria-selected="true"  aria-controls="panel-1" tabindex="0">HTML</button>
      <button role="tab" id="tab-2" aria-selected="false" aria-controls="panel-2" tabindex="-1">CSS</button>
      <button role="tab" id="tab-3" aria-selected="false" aria-controls="panel-3" tabindex="-1">JavaScript</button>
    </div>

    <div role="tabpanel" id="panel-1" aria-labelledby="tab-1" tabindex="0">
      <h3>HTML Fundamentals</h3>
      <p>HTML provides the structure of web pages.</p>
    </div>
    <div role="tabpanel" id="panel-2" aria-labelledby="tab-2" tabindex="0" hidden>
      <h3>CSS Styling</h3>
      <p>CSS controls visual presentation.</p>
    </div>
    <div role="tabpanel" id="panel-3" aria-labelledby="tab-3" tabindex="0" hidden>
      <h3>JavaScript Interactivity</h3>
      <p>JavaScript adds dynamic behaviour.</p>
    </div>

    <script>
      var tabs = Array.from(document.querySelectorAll('[role="tab"]'));

      tabs.forEach(function(tab, i) {
        tab.addEventListener('click', function() { activate(i); });
        tab.addEventListener('keydown', function(e) {
          if (e.key === 'ArrowRight') activate((i + 1) % tabs.length);
          if (e.key === 'ArrowLeft')  activate((i - 1 + tabs.length) % tabs.length);
          if (e.key === 'Home')       activate(0);
          if (e.key === 'End')        activate(tabs.length - 1);
        });
      });

      function activate(index) {
        tabs.forEach(function(t, j) {
          var on = j === index;
          t.setAttribute('aria-selected', String(on));
          t.tabIndex = on ? 0 : -1;
          document.getElementById(t.getAttribute('aria-controls')).hidden = !on;
        });
        tabs[index].focus();
      }
    </script>
  </body>
</html>

6. Common Mistakes

Positive tabindex values โ€” breaks tab order unpredictably

<input tabindex="3">
<input tabindex="1">
<input tabindex="2">

Let DOM order determine tab order; never use tabindex > 0

<input>  <!-- first in tab order -->
<input>  <!-- second -->
<input>  <!-- third -->

Opening modal without moving focus into it

<button onclick="modal.hidden=false">Open</button>

Move focus to first interactive element after opening

<button onclick="modal.hidden=false; modal.querySelector('button').focus()">Open</button>

7. Try It Yourself

▶ Try It Yourself

8. Quick Reference

tabindex Value Effect Use For
tabindex=”0″ Adds to tab order at natural DOM position Custom interactive widgets
tabindex=”-1″ Programmatically focusable; not in Tab order Modal containers, skip targets, composite items
tabindex > 0 Focused before all natural elements Avoid always โ€” breaks expected order
(omitted) Native elements focusable in DOM order Links, buttons, inputs, select, textarea
hidden attribute Removed from tab order and visually hidden Collapsed panels, inactive tabs

9. Quiz

🧠 Test Yourself

Which tabindex value makes an element focusable via JavaScript but removes it from the natural Tab sequence?





โ–ถ Try It Yourself