useListNavigation
Implements WAI-ARIA Roving Tabindex for keyboard navigation in a list. Used internally by ActionsDropdown.
Overview
useListNavigation manages keyboard focus within a list of items using the WAI-ARIA Roving Tabindex pattern. It handles Arrow Down/Up to move focus, Enter to select, and Tab prevention to keep focus inside the list.
It is used internally by ActionsDropdown. Use it directly when building a custom navigable list that doesn't fit the ActionsDropdown API.
Import
import { useListNavigation } from '@unflow/ui/hooks/useListNavigation';Parameters
| Param | Type | Description |
|---|---|---|
itemCount | number | Total number of items in the list |
onSelect | (index: number) => void | Called when Enter is pressed on a focused item |
Return value
| Field | Type | Description |
|---|---|---|
focusItem | (index: number) => void | Programmatically focus an item by index |
containerProps | { ref, onKeyDown } | Spread onto the list container |
getItemProps | (index: number) => object | Spread onto each list item |
getItemProps(index) returns
| Prop | Value | Description |
|---|---|---|
data-navigation-item | true | Required marker for the hook to locate items |
tabIndex | 0 (first item) or -1 | Roving tabindex |
Usage
Basic navigable list
const { containerProps, getItemProps, focusItem } = useListNavigation({
itemCount: options.length,
onSelect: (index) => selectOption(options[index]),
});
// Focus the first item when the list mounts
useEffect(() => { focusItem(0); }, []);
return (
<ul {...containerProps}>
{options.map((option, index) => (
<li key={option.id} {...getItemProps(index)}>
<button onClick={() => selectOption(option)}>
{option.label}
</button>
</li>
))}
</ul>
);Focus on open
A common pattern is to focus the first item when a dropdown opens:
const { containerProps, getItemProps, focusItem } = useListNavigation({
itemCount: items.length,
onSelect: handleSelect,
});
useEffect(() => {
if (isOpen) focusItem(0);
}, [isOpen]);Keyboard behavior
| Key | Behavior |
|---|---|
ArrowDown | Move focus to next item (wraps to first) |
ArrowUp | Move focus to previous item (wraps to last) |
Enter | Call onSelect with the focused item's index |
Tab | Blocked with preventDefault — focus stays inside the list |
Implementation notes
Items must have data-navigation-item. The hook uses querySelectorAll('[data-navigation-item]') on the container ref to find focusable items. This means the DOM order of items (not their indices) determines navigation order. If items are reordered in the DOM, navigation automatically follows.
Tab prevention. Tab is preventDefault'd inside the list. This is intentional for dropdowns and command palettes where the user should remain focused on the list until they either select an item or press Escape to close. For cases where Tab should cycle through items rather than escape the list, do not use this hook.
Roving tabindex. Only the currently-focused item has tabIndex={0}; all others have tabIndex={-1}. This means the list as a whole takes one tab stop from the browser's perspective, and arrow keys navigate within it — consistent with ARIA's composite widget pattern.
Accessibility
When using useListNavigation for a menu, set the container role:
<ul role="menu" {...containerProps}>
{items.map((item, i) => (
<li role="menuitem" {...getItemProps(i)} key={item.id}>
{item.label}
</li>
))}
</ul>