All hooks / Events and input

useRovingFocus

838 B minified and brotliedSourceMarkdown✓ Works in all modern browsers

Arrow-key navigation between the items of a toolbar, menu, tab list, listbox, or radio group, with a single Tab stop (a "roving tabindex"). Returns a callback ref for the container.

Tab enters the group at the last focused item (or the selected one, else the first) and Tab again leaves it. Arrow keys move between items, Home and End jump to the ends, and typing letters jumps to the item whose text starts with them. Left and Right swap in right-to-left content. Disabled and hidden items are skipped. Items added later are included.

Import

import { useRovingFocus } from '@vincent.ri/react-tils'
// or only this hook
import { useRovingFocus } from '@vincent.ri/react-tils/useRovingFocus'

Usage

const ref = useRovingFocus<HTMLDivElement>({ orientation: 'horizontal' })
return (
  <div ref={ref} role="toolbar" aria-label="Formatting">
    <button>Bold</button>
    <button>Italic</button>
    <button>Underline</button>
  </div>
)

Live demo

Tab into the toolbar, then use the arrow keys, or type a letter.

pressed: none

When to use it

A custom toolbar, menu, tab list, listbox, or segmented control.

When not to

Native elements do it: radio buttons with the same name, and <select>. Don't set tabIndex on the items yourself; the hook manages it. Still add the ARIA roles (role="toolbar", role="tab", …) for screen readers. Two-dimensional grids need more than this.

API

function useRovingFocus<T extends HTMLElement = HTMLElement>(options?: RovingFocusOptions): (element: T | null) => void
ParameterDescription
optionsorientation, loop, typeahead, selector. Optional.

Returns A callback ref. Pass it to the container's ref prop.

Types

export type RovingFocusOptions = {
  /** Which arrow keys move focus. Default `'vertical'` (Up/Down). */
  orientation?: "vertical" | "horizontal" | "both";
  /** Wrap from the last item to the first and back. Default `true`. */
  loop?: boolean;
  /** Typing letters moves to the next item whose text starts with them; repeating one letter cycles through matches. Default `true`. */
  typeahead?: boolean;
  /** Which elements inside the container are items. Default: buttons, links, and elements with an item role (`menuitem`, `option`, `tab`, `radio`, `treeitem`). */
  selector?: string;
};

Browser support

UsesChromeEdgeFirefoxSafariChrome AndroidSafari iOS
MutationObserverWidely available26+12+14+7+26+7+
checkVisibilityoptionalWidely available105+105+106+17.4+105+17.4+
inertWidely available102+102+112+15.5+102+15.5+

Optional features are used when present; without them the hook still works.

First version with support, from MDN's compatibility data (web-features 3.42.0, @mdn/browser-compat-data 8.1.5).

Coming from another library?

Other libraries call this useRovingTabIndex.