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.
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
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.
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.)
tabindex variations
tabindex="0" on a div makes it tabbable; tabindex="-1" is
focusable-but-not-tabbable (gets no number).
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.
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.
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.