All hooks / Events and input
useRovingFocus
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.
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| Parameter | Description |
|---|---|
options | orientation, 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
| Uses | Chrome | Edge | Firefox | Safari | Chrome Android | Safari iOS |
|---|---|---|---|---|---|---|
| MutationObserverWidely available | 26+ | 12+ | 14+ | 7+ | 26+ | 7+ |
| checkVisibilityoptionalWidely available | 105+ | 105+ | 106+ | 17.4+ | 105+ | 17.4+ |
| inertWidely available | 102+ | 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.