Out of Order

GitHub
What's tabbable

What lands in the tab order

The tabbable edge cases worth knowing, and whether we count each in the tab order. The overlay numbers what is tabbable; everything else gets no number. For the bugs the package flags, see the playground.

Standard focusable controls

The baseline: link, button, text input, select, textarea.

Link with href

Media & embedded content

A media element with controls is a single tab stop. What sits inside it, like play, scrubber, and volume, is built by the browser itself, and every browser arranges those differently, so there's no knowing how focus moves through them. That doesn't matter here: we count the element as one stop, and that holds true whichever browser you're in. An <iframe> is focusable too, but tabbable deliberately leaves it out of the tab order (it can't reason about the frame's own stops), so here it gets no number, even though browsers do tab into it. Without controls, <audio>/<video> aren't focusable at all.

Disclosure: details / summary

A <details> puts its <summary> in the tab order as the toggle. While closed, the content is hidden and only the summary is tabbable; when open, any focusable content inside joins the sequence too.

Closed disclosure, summary is the tab stop

This text is hidden while closed, so it adds nothing to the tab order.

Open disclosure
Link inside open details

Image-map areas

An <area> with an href inside a <map> is tabbable like a link. An <area> with no href is not focusable, and one with tabindex="-1" is focusable but not tabbable, so only the first region below gets a number.

image map with three regions region with href region with no href (skipped) region with negative tabindex (skipped)

Radio group with a selection: one tab stop

Once a radio group has a checked option, it collapses to a single tab stop: only the checked radio lands in the tab order, and the others are reached with the arrow keys, so three radios below give one number. (A group with no selection is different: tabbable keeps every radio in the order.)

Shipping

tabindex variations

tabindex="0" on a div makes it tabbable; tabindex="-1" is focusable-but-not-tabbable (gets no number).

div[tabindex=0] role=button
div[tabindex=-1] (skipped)

Focusable, but not tabbable

tabindex="-1" keeps an element focusable by script or a mouse click, but Tab steps over it. Even natively-tabbable controls drop out of the sequence, so every element here gets no number.

a[href][tabindex=-1]

Disabled & inert (not tabbable)

Disabled controls and the children of an [inert] subtree get no number.

Disabled fieldset: the first legend stays live

Everything inside a disabled <fieldset> drops out of the tab order, except controls inside its first <legend>, which stay enabled. So the legend button below is tabbable while the body field next to it is not.

Hidden elements (not tabbable)

display:none, visibility:hidden and the hidden attribute remove an element from the tab order, so they get no number. (Other ways of hiding that don't, such as opacity, off-screen, and clipping, are bugs; see the playground.)

Links, readonly & contenteditable

A bare <a> with no href is not tabbable; readonly inputs and contenteditable regions are.

SVG & shadow DOM

A focusable <svg> link, plus a focusable element inside an open shadow root (resolved via tabbable's getShadowRoot, injected by JS).

Keyboard-focusable scroll container (a tabbable blind spot)

A scroll container with overflowing content and no focusable children is keyboard-focusable in modern browsers, even without tabindex: you can tab to it and scroll with the arrow keys. tabbable doesn't detect this, though, so on this page it gets no number: a real tab stop the library can't see.

This box scrolls. It has no buttons, links, or inputs inside, so the browser makes the container itself keyboard-focusable so it can be scrolled with the arrow keys. Keep reading to overflow it: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.

Virtual list: a tab order rewritten every frame

1,000 rows, but only the visible handful are in the DOM. Scrolling recycles rows in and out, rewriting the tab sequence every frame, and the overlay renumbers in step. Watch it keep up with a DOM that never sits still.