tabbable: Identify Tabbable DOM Nodes
tabbable is a JavaScript utility library designed to accurately identify and return an array of all keyboard-tabbable DOM nodes within a specified container element. It systematically determines tabbability based on standard HTML semantics (e.g., `<button>`, `<input>`, `<a>` with `href`), explicit `tabindex` attributes, and various visibility and accessibility rules. The library is actively maintained, currently at version `6.4.0`, and receives regular minor and patch updates to enhance browser compatibility, support new web standards like the `inert` attribute, and address issues in virtual DOM environments like JSDOM. Its key differentiators include a zero-dependency footprint, small bundle size, high accuracy in diverse scenarios, and optimized performance. It supports a broad range of modern desktop browsers (Chrome, Edge, Firefox, Safari, Opera), but crucially, it dropped support for Internet Explorer browsers starting with v6.0.0. The library also provides granular control over how visibility checks are performed via the `displayCheck` option, accommodating various application needs.
Common errors
-
focus-trap must have at least one tabbable node in it
cause This error often originates from `focus-trap` (which uses `tabbable`) when the underlying `tabbable` library cannot find any tabbable elements in a JSDOM environment or due to incorrect `inert` attribute handling.fixEnsure your JSDOM setup is current (v26+). Verify that elements within the trapped container are not inadvertently hidden or marked `inert` in a way not supported by your browser/JSDOM version. Consider adding `tabindex="0"` to explicitly make an element tabbable if it should be. -
TypeError: Cannot read properties of undefined (reading 'getRootNode')
cause This crash occurs when a DOM node being evaluated by `tabbable` is detached from the document, causing `isHidden()` to fail during a call to `getRootNode()`.fixEnsure that any DOM nodes passed to `tabbable` (or its internal functions) are attached to the document. This issue was largely fixed in `v6.1.0` to handle detached nodes more gracefully, but may still manifest in specific edge cases or older versions.
Warnings
- breaking Support for Internet Explorer (all versions) has been officially dropped. The library no longer guarantees functionality or provides fixes for IE environments.
- gotcha The `inert` HTML attribute, which prevents focus and interaction, is not consistently supported across all major browsers (notably Firefox and Safari as of February 2023). While `tabbable` includes checks for `inert`, its effectiveness depends on browser-level support.
- gotcha When running in JSDOM environments, versions prior to v26 may exhibit issues with CSS selectors related to the `inert` attribute, leading to incorrect identification of tabbable nodes. `tabbable` v6.4.0 re-enabled a CSS selector fast path for `inert`.
- gotcha The default `displayCheck='full'` option may inaccurately determine all nodes are hidden if the container element is not attached to the document. In such cases, `tabbable` may revert to `displayCheck='none'` behavior.
- gotcha Very old browser environments might require a polyfill for the `CSS.escape` API, especially if you have radio buttons with special characters in their `name` attributes. Without it, `tabbable` might not work correctly with such elements.
Install
-
npm install tabbable -
yarn add tabbable -
pnpm add tabbable
Imports
- tabbable
const { tabbable } = require('tabbable');import { tabbable } from 'tabbable'; - focusable
const focusable = require('tabbable').focusable;import { focusable } from 'tabbable'; - getTabIndex
import { getTabIndex } from 'tabbable/dist/getTabIndex';import { getTabIndex } from 'tabbable';
Quickstart
import { tabbable, focusable, getTabIndex } from 'tabbable';
// Helper to create a DOM structure for testing in a browser or JSDOM environment
function createTestDOM(htmlString) {
const container = document.createElement('div');
container.innerHTML = htmlString;
// Append to body to ensure elements are considered 'attached' for visibility checks
document.body.appendChild(container);
return container;
}
const testHtml = `
<div id="root-container">
<button id="btn1">Click Me</button>
<input type="text" placeholder="Enter text" />
<a href="#" id="link1">A link</a>
<span tabindex="0" id="span1">Custom tabbable</span>
<div tabindex="-1" id="div1">Focusable but not tabbable</div>
<button disabled id="btn2">Disabled Button</button>
<a href="#" style="display: none;" id="link2">Hidden Link</a>
<p>Some text</p>
<textarea id="textarea1"></textarea>
</div>
`;
const containerElement = createTestDOM(testHtml);
console.log('--- Tabbable elements ---');
const tabbableElements = tabbable(containerElement);
tabbableElements.forEach(el => {
console.log(`Tabbable: ${el.outerHTML}, TabIndex: ${getTabIndex(el)}`);
});
console.log('\n--- Focusable elements (including non-tabbable) ---');
const focusableElements = focusable(containerElement);
focusableElements.forEach(el => {
console.log(`Focusable: ${el.outerHTML}, TabIndex: ${getTabIndex(el)}`);
});
// Clean up the added DOM element
document.body.removeChild(containerElement);