Keyboard accessibility for custom controls

Native HTML elements like <button> and <input> ship with keyboard support out of the box, so they should always be your first choice for interactive content. When you need a bespoke widget, however, the tabindex attribute is the tool that lets you recreate the expected focus behavior users rely on.

How tabindex works

The tabindex attribute accepts integer values that control where an element sits in the tab order. Use it to reorder focusable elements, add an otherwise non-focusable element to the tab sequence, or pull an element out of that sequence entirely.

  • tabindex="0" places the element in the natural tab order, making it focusable via the Tab key and through the element's focus() method.
  • tabindex="-1" removes the element from the natural tab order while keeping it programmatically focusable via focus().
  • A value greater than 0 (tabindex="5", for example) moves the element to the front of the tab order. If several elements have positive tabindex values, they are visited from the smallest value up.

Positive tabindex values are an anti-pattern. Screen readers traverse the page in DOM order, not tab order, so an element promoted with tabindex > 0 can cause confusing mismatches between the reading sequence and the visible focus sequence.

Check and fix focusability manually

Automated tools such as Lighthouse detect many accessibility problems, but not all of them. Some checks require human judgment. Press the Tab key and move through your site: can you reach every interactive control? If not, you likely need to adjust tabindex on those controls.

Page-level focus management

In single-page layouts with content sections that swap without a page refresh, the visual context can change while the user's keyboard focus stays behind. To sync focus with the newly visible content, give the active content area a tabindex of -1 and call its focus() method. The -1 value keeps the region out of the natural tab order, so this managing focus technique doesn't introduce extra stops for keyboard users.

Component-level focus patterns

For custom elements and composite widgets, focus management typically needs to be handled at the control level. The ARIA Authoring Practices guide is the reference for which keyboard behaviors each component type should implement.

Joining and leaving the tab order

Add an element to the natural tab order with tabindex="0" so it can be focused by pressing Tab or by calling focus():

<div tabindex="0">Focus me with the TAB key</div>

To pull an element out of the natural tab order while retaining programmatic focus capability, use tabindex="-1":

<button tabindex="-1">Can't reach me with the TAB key!</button>

Note that tabindex="-1" does not affect child elements. A child that is naturally focusable, or that carries its own positive tabindex, stays in the tab order. If you need to remove an entire subtree from the tab order, consider the WICG's inert polyfill, which emulates a proposed attribute that prevents elements from being selected or announced by assistive technologies.

The dangers of positive tabindex

You should avoid tabindex values greater than 0 because of the DOM-order versus tab-order mismatch. If you find yourself wanting to move an element earlier in the tab sequence, the correct fix is to move it to an earlier position in the DOM.

Lighthouse can flag offending elements automatically. Run the Accessibility audit under Lighthouse > Options > Accessibility and look for the "No element has a [tabindex] value greater than 0" check.

The roving tabindex technique

For complex components, you may need arrow-key support that goes beyond simple focus. When feasible, the native <select> element already provides focus management and arrow keys for option selection.

To recreate this in a custom widget, employ roving tabindex. Start by setting tabindex to -1 on every child except the active one. A keyboard event listener then tracks which key the user pressed:

<div role="toolbar">
  <button tabindex="-1">Undo</button>
  <button tabindex="0">Redo</button>
  <button tabindex="-1">Cut</button>
</div>
<div role="toolbar">
  <button tabindex="-1">Undo</button>
  <button tabindex="-1">Redo</button>
  <button tabindex="0">Cut</button>
</div>

Inside the listener, the component sets the previously focused child to tabindex="-1", assigns the new target child tabindex="0", and calls focus() on that child. The result is a single-tab-stop component that exposes full navigation through the arrow keys.

Referencing keyboard support patterns

The ARIA Authoring Practices 1.1 spec provides a catalogue of common UI patterns and the specific keys they are expected to support. When building a custom control, use it to determine the required keyboard surface instead of guessing.