UnflowUI
ComponentsHooks

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

ParamTypeDescription
itemCountnumberTotal number of items in the list
onSelect(index: number) => voidCalled when Enter is pressed on a focused item

Return value

FieldTypeDescription
focusItem(index: number) => voidProgrammatically focus an item by index
containerProps{ ref, onKeyDown }Spread onto the list container
getItemProps(index: number) => objectSpread onto each list item

getItemProps(index) returns

PropValueDescription
data-navigation-itemtrueRequired marker for the hook to locate items
tabIndex0 (first item) or -1Roving 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

KeyBehavior
ArrowDownMove focus to next item (wraps to first)
ArrowUpMove focus to previous item (wraps to last)
EnterCall onSelect with the focused item's index
TabBlocked 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>