tabbable

repository·master·Indexed 20 days ago

https://github.com/focus-trap/tabbable

A small, fast, and dependency-free utility for identifying all tabbable and focusable DOM nodes within a container. Version 6.5.0 provides APIs such as tabbable() to find elements in tab order, focusable() for DOM order, isTabbable(), isFocusable(), and getTabIndex(). It includes configurable display checks and Shadow DOM support to help developers manage keyboard navigation and focus traps across different browsers.

Tokens
4.1K
Snippets
13
Records
18
Agent score
71%

What's inside tabbable

  1. What is considered a tabbable element?

    master

    By default, tabbable considers the following elements as tabbable:

    • <button> elements
    • <input> elements
    • <select> elements
    • <textarea> elements
    • <a ext> elements with an href attribute
    • <area> elements with an href attribute (inside a <map> referenced by a rendered image)
    • <audio> and <video> elements with controls attributes
    • The first <summary> element directly under a <details> element
    • <details> elements without a <summary> element
    • Elements with the [contenteditable] attribute
    • Anything with a non-negative tabindex attribute

    Elements are NOT considered tabbable if:

    • They have a negative tabindex attribute
    • They have a disabled attribute
    • They (or an ancestor) are hidden via display: none (unless modified via displayCheck option)
    • They have visibility: hidden or visibility: collapse style
    • They are nested under a closed <details> element (except the first <summary>)
    • They are an <input type="radio"> and a different radio in its group is checked
    • They are a form field inside a disabled <fieldset>
    • They are inert or in an inert container
  2. Best practices for tabbability and accessibility

    master

    When using Tabbable, keep the following accessibility and browser consistency tips in mind:

    • Edge-case elements: Elements like <iframe />, <embed />, <object />, <summary />, and <svg /> have inconsistent tabbability across browsers. To ensure they are reliably detected, add tabindex="0" to them.
    • Radio groups: To avoid edge cases where all radios in a group are considered tabbable, ensure the group always has one checked radio.
    • Avoid positive tabindexes: Do not use tabindex values greater than 0. Rely on the natural document order for accessibility.
    • Safari/Mac: Safari on Mac may not tab to <a> elements by default due to user settings. Tabbable includes them in the list regardless of local browser settings.
  3. Handle testing in JSDom (Jest)

    master

    JSDom is not officially supported and does not fully implement the DOM APIs (like Element.getClientRects()) used by Tabbable to determine visibility. To prevent tests from failing due to incorrect visibility calculations, it is highly recommended to set displayCheck: 'none' when using Tabbable in JSDom environments.

    You can globally mock Tabbable in Jest by creating a file in your __mocks__ folder to force displayCheck: 'none' on all API calls.

    // __mocks__/tabbable.js
    
    const lib = jest.requireActual('tabbable');
    
    const tabbable = {
       ...lib,
       tabbable: (node, options) => lib.tabbable(node, { ...options, displayCheck: 'none' }),
       focusable: (node, options) => lib.focusable(node, { ...options, displayCheck: 'none' }),
       isFocusable: (node, options) => lib.isFocusable(node, { ...options, displayCheck: 'none' }),
       isTabbable: (node, options) => lib.isTabbable(node, { ...options, displayCheck: 'none' }),
    };
    
    module.exports = tabbable;
  4. Install tabbable via npm

    master

    To use tabbable in your project, install it using npm:

    npm install tabbable

    Note: Some very old browsers may require a polyfill for the CSS.escape API to ensure proper handling of radio buttons with special characters in their name attributes.

  5. Configure the `getShadowRoot` option

    master

    By default, Tabbable ignores all elements inside Shadow DOMs. Use getShadowRoot to enable Shadow DOM support.

    Type: boolean | (node: FocusableElement) => ShadowRoot | boolean | undefined

    Options:

    • false (default): Disables Shadow DOM support for calculated tab order and closed shadow roots. However, if a child of a shadow (open or closed) is passed directly to isTabbable() or isFocusable(), the shadow DOM is still used for visibility checks.
    • true: Enables support for any open shadow roots. It does not attempt to find closed shadow roots.
    • function: Allows custom logic to find shadow roots. The function receives a node (a descendant of the container).
      • Return the ShadowRoot to enable traversal.
      • Return true to indicate a closed ShadowRoot is attached but inaccessible (undisclosed). This causes Tabbable to treat the node as a scope and use the non-zero-area display check for its children.
      • Return a falsy value if no shadow is attached.
    // Example: Enabling Shadow DOM support via a function
    tabbable(container, {
      getShadowRoot: (node) => node.shadowRoot
    });
  6. Configure the `displayCheck` option

    master

    The displayCheck option configures how Tabbable determines if an element is displayed. This option is available to all APIs. Note that most options (except none) may cause layout reflow because they use Web APIs to validate visibility.

    Available values:

    • full (default): Resembles browser behavior via manual checks. Requires the element and all ancestors to be attached to the DOM and displayed. Note: As of v6.0.0, if the node or its container is not attached to the window's main document, it is considered hidden.
    • full-native: Uses the browser's built-in Element#checkVisibility. It handles content-visibility: auto, visibility: hidden, and zero-size elements. Falls back to full if not supported.
    • legacy-full: Restores the behavior from versions prior to v6.0.0 where detached nodes were treated as visible. Not recommended as it diverges from browser behavior.
    • non-zero-area: Assumes non-displayed elements have zero width and height. Detached nodes are always considered hidden in this mode because the browser does not calculate dimensions for them.
    • none: Completely opts out of the display check. Not recommended as it may return elements that are not actually tabbable/focusable, potentially breaking accessibility.
    // Example of passing the option to an API
    tabbable(container, { displayCheck: 'full-native' });
  7. Check if a node is focusable with `isFocusable()`

    master

    Use isFocusable to determine if a specific DOM node can receive focus.

    Key Distinction: All tabbable elements are focusable, but not all focusable elements are tabbable (e.g., elements with tabindex="-1" are focusable but not tabbable).

    Parameters:

    • node (Node): The node to check.
    • options (Object, optional): Configuration options.

    Note: If the node has an inert ancestor, it is not considered focusable.

    import { isFocusable } from 'tabbable';
    
    const focusable = isFocusable(node, { /* options */ });
  8. Get a node's tab index with `getTabIndex()`

    master

    Use getTabIndex to retrieve the numeric tab index of an element.

    Returns: A negative, 0, or positive number representing the node's tab index in the DOM.

    Note: There are specific exceptions for <audio>, <video>, <details>, and elements with contenteditable="true" due to browser inconsistencies. The implementation handles these cases specifically.

    import { getTabIndex } from 'tabbable';
    
    const index = getTabIndex(node);
  9. Check if a node is tabbable with `isTabbable()`

    master

    Use isTabbable to determine if a specific DOM node can be reached via keyboard navigation (tabbing).

    Parameters:

    • node (Node): The node to check.
    • options (Object, optional): Configuration options (see Common Options).

    Note: If the node has an inert ancestor, it is not considered tabbable.

    import { isTabbable } from 'tabbable';
    
    const tabbable = isTabbable(node, { /* options */ });
  10. Get all focusable nodes with `focusable()`

    master

    Use focusable to retrieve an array of all focusable DOM nodes within a container.

    Important: This returns nodes in DOM order, which is different from the tab order returned by tabbable().

    Parameters:

    • container (Node): The root node to search within.
    • options (Object, optional):
      • includeContainer (boolean): If true, the container itself is included in the array if it is focusable. Defaults to false.

    Note: If the container is inert, none of its children will be considered focusable.

    import { focusable } from 'tabbable';
    
    const focusableNodes = focusable(container, { /* options */ });
  11. Get all tabbable nodes with `tabbable()`

    master

    Use the tabbable function to retrieve an array of all tabbable DOM nodes within a specific container, returned in the correct tab order.

    Parameters:

    • container (Node): The root node to search within.
    • options (Object, optional): Configuration options.
      • includeContainer (boolean): If true, the container itself is included in the array if it is tabbable. Defaults to false.

    Ordering Logic:

    1. Nodes with positive tabindex (1 or higher), ordered by ascending tabindex and then source order.
    2. Nodes with `tabindex=
  12. Find all tabbable elements with tabbable()

    master

    Use tabbable(container, options) to find all elements within a container that can be reached via keyboard tab navigation. The returned array is sorted by tab order (respecting tabindex).

    Options:

    • includeContainer (boolean): If true, includes the container itself in the results if it is tabbable.
    • getShadowRoot (GetShadowRoot | boolean): Enables Shadow DOM support. If a function, it should return the ShadowRoot or true if an undisclosed shadow root exists.
    • shadowRootFilter (ShadowRootFilter): A function to filter shadow hosts.
    • flatten (boolean): (Used internally by iterative logic) determines if CandidateScope objects are flattened into the result list.
    import { tabbable } from 'tabbable';
    
    const elements = tabbable(container, { includeContainer: true });