Accessibility · Keyboard and Focus

Which Elements Can Actually Take Focus

The closed list of natively focusable elements, the three independent exclusions that remove an element from that list, and why a call to focus() can return without moving focus at all.

Focusability is not a style and not a boolean an author sets. It is a property the browser derives from element type, attributes, and rendering state, and the derivation has a precise shape: a closed list of element types, plus one attribute that overrides the list, minus three independent exclusions. Getting the three layers confused produces the most common keyboard bug in existence — a control that looks interactive, responds to a mouse, and cannot be reached by Tab.

The list the specification gives

The HTML standard defines a focusable area in two tiers. The first is a general criterion: an element is a focusable area if its tabindex value is non-null, or if the user agent determines it to be focusable, provided it is not a shadow host with focus delegation, not actually disabled, not inert, and is being rendered. The second is a list of element types the specification suggests user agents treat as focusable and sequentially focusable:

ElementCondition
ahas an href attribute
button, select, textareaalways
inputtype is not hidden
summaryfirst summary element child of a details
iframe and other navigable containersalways
any elementis a drag source with draggable set, where that helps
any elementis an editing host
any elementhas a non-null tabindex value

The wording matters for two entries. object and embed are not in the list; they are covered only through the navigable-container language and through platform conventions. audio and video are not either — a media element with controls is focusable in practice, but by user-agent discretion rather than by a specified rule, and its internal controls are not DOM elements at all, so they cannot be reached with querySelector or selectively hidden.

Anything else — a div with a click handler, a span styled as a button, an SVG <g> — is not focusable. The list is short and it has not grown to accommodate popular patterns. A div that behaves like a button remains unfocusable until an author adds tabindex, at which point it enters the list through the general criterion rather than through its type.

The document’s body element is a special case: document.activeElement returns it when the focused area is the document’s viewport, which is the state of a freshly loaded page. It is not null because nothing is focused; it is the body because that is what the getter substitutes.

Three exclusions, applied independently

Being on the list is necessary but not sufficient. Three separate mechanisms remove an element from the sequence, and they are not interchangeable.

Not rendered. An element in a subtree with display: none, or with visibility: hidden, or inside a closed content-visibility: hidden container, is not being rendered, and a non-rendered element has no focusable area. The hidden attribute is exactly this case, since the user agent stylesheet maps it to display: none. Note that visibility: hidden on a child of a visible parent removes that child while leaving the parent focusable, and that visibility: visible on a descendant re-enables it — visibility is inherited and can be overridden, display cannot.

Inert. The inert attribute, applied to an element or inherited from an ancestor, removes the entire subtree from the tab sequence, from the accessibility tree, from hit testing, and from find-in-page. It is a single switch with four effects, and it is the only mechanism that removes a subtree from both the keyboard sequence and the accessibility tree at once. aria-hidden="true" removes only the accessibility-tree half, which is why pairing aria-hidden with focusable content creates a control that a keyboard user can reach while a screen reader reports nothing there.

Disabled. An element is actually disabled only if it is one of a short list: a button, input, select, or textarea that is disabled, an option or optgroup disabled itself or through its select, a disabled fieldset, or a disabled form-associated custom element. Those elements are not focusable areas. The attribute is inherited within a form: a control inside a disabled fieldset is disabled, except that controls inside the fieldset’s first legend remain enabled. aria-disabled="true" is not the same thing: it communicates state without removing the control from the tab sequence, which is the correct choice when a disabled control needs to remain discoverable, and the wrong choice when the control is a submit button that should not be reachable at all.

The three exclusions compose. A disabled button inside an inert container inside a display: none dialog is excluded three times over; removing any one of them does not make it focusable while the others hold.

Why focus() silently fails

HTMLElement.focus() has no return value and no error. When the target has no focusable area, the call does nothing: focus stays where it was, and document.activeElement is unchanged. This is the single most misleading behaviour in the focus API, because the failure is invisible at the call site.

const panel = document.querySelector('#panel'); // currently display: none
const field = panel.querySelector('input');

field.focus();
console.log(document.activeElement === field); // false — the call was ignored

// A reliable sequence: make it renderable first, then focus, then confirm.
panel.hidden = false;
field.focus();
if (document.activeElement !== field) {
  // Focus was refused anyway: the element is disabled, inert, or in another document.
  panel.querySelector('[tabindex="-1"]')?.focus();
}

The confirmation step is not paranoia. Focus can also be refused when the element is inside an iframe whose document has not finished loading, when the element belongs to a different document than the caller, and when the browser’s own focus is outside the page — a background tab does not move its document’s focus in response to script. Reading back document.activeElement is the only portable check.

A subtler case: focusing an element inside a container that is about to become visible. A common pattern unhides a dialog and focuses its first field in the same task. If the styles are applied by a stylesheet that has not yet been evaluated for the new subtree — or if the dialog uses a transition and the element is still at zero size — the element may not be rendered at the moment of the call. Focusing after a frame, or focusing a container that is already rendered and letting the browser descend, avoids the race.

tabindex as an override, not a grant

The tabindex attribute does two unrelated jobs, and conflating them causes confusion.

For an element already on the list, tabindex changes position. A tabindex="0" on a button changes nothing observable. A positive value moves the element into the first ordering pass, as described in How the Browser Computes the Tab Sequence, and a negative value removes it from the sequence while leaving it focusable from script.

For an element not on the list, tabindex changes existence. Adding tabindex="0" to a div creates a focusable area where none existed. This is the mechanism behind every custom widget, and it carries an obligation the browser does not enforce: the element now takes focus, but it has no keyboard behaviour, no role, and no name. The focus ring appears around something that does not respond to Enter or Space, and because the element is not a native control the ring’s appearance is governed by the :focus-visible heuristic described in Focus Rings, :focus-visible, and the Heuristic Behind Them.

tabindex="-1" on a non-listed element is the narrower and more useful form. It creates a focusable area that the Tab key skips, which is what a container needs in order to receive focus programmatically after navigation or after a dialog closes. The distinction between “reachable by Tab” and “reachable by script” is the whole of the attribute’s semantics; the numbers above zero are a legacy ordering facility that authors should treat as unavailable.

Parsing has one wrinkle. The tabindex value is the attribute parsed as an integer, and a value that fails to parse leaves the element with a null tabindex value — the same value an omitted attribute produces. The specification does not then declare the element focusable; it delegates the decision to platform conventions. The tabIndex IDL attribute reports -1 in that case, which is also what it reports for a genuine negative index, so the two states are not distinguishable from script even though they mean different things.

Clicking is not focusing everywhere

Browsers do not uniformly move focus when a mouse is used. Clicking a focusable element generally focuses it on the major desktop engines, with the documented exception of some WebKit builds, where clicking a button or a link does not focus it at all. The consequence is that a component which only implements click handlers behaves correctly for mouse users and incorrectly for keyboard users in a way that no amount of testing with a mouse reveals.

For custom widgets the ordering of events matters here too: focus typically arrives on mousedown, before click, so a handler that reads document.activeElement during click sees the element already focused. Widgets that rely on that ordering to track which item was activated break when activation arrives from the keyboard instead, because no pointer event occurs. Tracking activation through the element that received the event, rather than through the currently focused element, removes the dependency.

The practical rule that follows from all of this: an element either is on the focusability list and behaves like its type, or it is not on the list and needs tabindex plus a role, a name, and key handlers. There is no third category, and a control that responds to the mouse without appearing on either side of that line is unreachable by design.

References

  1. HTML Living Standard — Focusable areas — WHATWG
  2. HTML Living Standard — The inert attribute — WHATWG
  3. HTML Living Standard — Enabling and disabling form controls — WHATWG
  4. MDN Web Docs — HTMLElement.focus() — MDN