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:
| Element | Condition |
|---|---|
a | has an href attribute |
button, select, textarea | always |
input | type is not hidden |
summary | first summary element child of a details |
iframe and other navigable containers | always |
| any element | is a drag source with draggable set, where that helps |
| any element | is an editing host |
| any element | has 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.