ADA Compliance Professionals

    Tabindex over 0 breaks logical focus

    Last updated:

    Who it helps:
    Mobility
    Standard:
    WCAG 2.2 Level A

    Focusable elements must not use tabindex greater than 0

    Do not assign positive tabindex values to any element. Positive values force a custom tab sequence that often conflicts with the reading order and confuses keyboard and assistive technology users.

    This issue appears in custom components, single-page apps, and layouts that rely on visual reordering. It affects people who navigate by keyboard, screen reader users, and anyone relying on a predictable focus path.

    Why It Matters

    Tabbing is how many users explore and operate a page. When focus jumps in an unexpected order, people miss controls, lose context, or give up.

    Users with mobility impairments depend on efficient keyboard navigation. Screen reader users expect focus to progress in the same order content is read. Cognitive load increases when focus moves unpredictably.

    This maps to WCAG 2.2: 2.4.3 Focus Order, 2.1.1 Keyboard, and 1.3.2 Meaningful Sequence.

    Common Causes

    • Using tabindex="1", "2", etc. to force a particular focus path.
    • Visual-only reordering (flexbox order, grid areas, absolute positioning) that doesn’t match DOM order.
    • Making non-interactive elements (div, p, span) focusable without a valid interaction.
    • Custom widgets built from generic elements instead of native controls.
    • Framework components that inject positive tabindex by default.

    How to Fix

    1. Remove all positive tabindex values.
    2. Use native interactive elements whenever possible:
      • Buttons for actions (<button>), links for navigation (<a href>), form controls for input.
    3. If you must make a non-native control focusable, set tabindex="0" and provide proper role, name, and keyboard behavior. For composite widgets (e.g., tabs, menus), use the roving tabindex pattern so only one child is in the tab sequence at a time.
    4. Align focus order with DOM order. Reorder the source markup to match the visual design rather than relying on CSS reordering.
    5. Use tabindex="-1" only for programmatic focus targets (e.g., dialog headings, error summaries, landmarks) and move focus with script at appropriate times.
    6. Ensure visible focus styles remain clear and consistent (WCAG 2.4.7/2.4.11/2.4.12 as relevant).

    Recommendation: Avoid CSS techniques that change visual order (e.g., order in flexbox, grid re-placement, absolute positioning) unless the DOM order is updated to match.

    How to Test

    Keyboard check:

    • Press Tab across the page. Confirm focus moves in a logical, top-to-bottom, left-to-right order that matches reading order.
    • Use Shift+Tab to move backward; the sequence should reverse predictably.
    • Ensure only actionable elements are focusable; static text should not receive focus.
    • Confirm there are no focus skips, loops, or dead-ends, and that focus is always visible.

    Screen reader check (NVDA/JAWS/VoiceOver):

    • Tab through interactive elements and verify announcements make sense and follow a logical order.
    • Use reading commands to confirm that reading order and focus order align.

    Mobile/touch and external keyboard:

    • On iOS/Android with a hardware keyboard, tab through controls and confirm the same logical order.

    Quick audit in DevTools/console:

    CSS
    [...document.querySelectorAll('[tabindex]')]
      .filter(el => Number(el.getAttribute('tabindex')) > 0)

    If any elements are returned, remove or refactor those tabindex values.

    Automated checks:

    • Run axe, ESLint (jsx-a11y), or similar rules to flag tabindex > 0.

    Good Example

    A tabs interface using native buttons and roving tabindex (only the active tab is tabbable):

    HTML
    <div role="tablist" aria-label="Payment method">
      <button role="tab" id="tab-card" aria-selected="true" aria-controls="panel-card" tabindex="0">Card</button>
      <button role="tab" id="tab-bank" aria-selected="false" aria-controls="panel-bank" tabindex="-1">Bank</button>
      <button role="tab" id="tab-paypal" aria-selected="false" aria-controls="panel-paypal" tabindex="-1">PayPal</button>
    </div>
    <section id="panel-card" role="tabpanel" aria-labelledby="tab-card">...</section>
    <section id="panel-bank" role="tabpanel" aria-labelledby="tab-bank" hidden>...</section>
    <section id="panel-paypal" role="tabpanel" aria-labelledby="tab-paypal" hidden>...</section>
    <script>
      const tabs = Array.from(document.querySelectorAll('[role="tab"]'));
      function activate(i) {
        tabs.forEach((t, idx) => {
          const selected = idx === i;
          t.setAttribute('aria-selected', selected);
          t.tabIndex = selected ? 0 : -1;
          const panel = document.getElementById(t.getAttribute('aria-controls'));
          panel.hidden = !selected;
        });
        tabs[i].focus();
      }
      tabs.forEach((t, i) => {
        t.addEventListener('keydown', e => {
          if (e.key === 'ArrowRight') activate((i + 1) % tabs.length);
          if (e.key === 'ArrowLeft') activate((i - 1 + tabs.length) % tabs.length);
        });
        t.addEventListener('click', () => activate(i));
      });
    </script>

    Bad Example

    Forcing a custom order with positive tabindex and making static text focusable:

    HTML
    <header>
      <div tabindex="1">Logo</div>
      <a href="#features" tabindex="3">Features</a>
      <a href="#pricing" tabindex="2">Pricing</a>
      <p tabindex="4">Welcome to our site</p>
    </header>

    This skips around illogically, focuses non-interactive text, and conflicts with DOM order.

    Quick Checklist

    • No element uses tabindex greater than 0.
    • DOM order matches the intended visual and reading order.
    • Only interactive controls are focusable in the Tab sequence.
    • Use native elements first; add tabindex="0" only when necessary for custom widgets.
    • Use tabindex="-1" solely for scripted focus targets (e.g., dialogs, error summary).
    • Composite widgets use roving tabindex (one tabbable child at a time).
    • Focus is always visible and does not jump unexpectedly forwards or backwards.
    • Automated and manual keyboard tests show a clear, predictable sequence.