Skip to content

useListNavigation

Adds arrow key-based navigation of a list of items, either using real DOM focus or virtual focus.

import {useListNavigation} from '@floating-ui/react';
import {useListNavigation} from '@floating-ui/react';

See FloatingList for creating composable children API components.

Usage

This hook is an interaction hook that returns event handler props and ARIA attribute props.

To use it, pass it the contextcontext object returned from useFloating()useFloating(), and then feed its result into the useInteractions()useInteractions() array. The returned prop getters are then spread onto the elements for rendering. getItemProps()getItemProps() is spread to each list item.

The listReflistRef holds an array of HTML elements. The activeIndexactiveIndex determines which index of the list is currently active (focused or highlighted).

When using real DOM focus (default), the list items must be focusable and should have an appropriate rolerole prop based on the role of the floating element.

function App() {
  const [activeIndex, setActiveIndex] = useState(null);
 
  const {refs, floatingStyles, context} = useFloating({
    open: true,
  });
 
  const listRef = useRef([]);
 
  const listNavigation = useListNavigation(context, {
    listRef,
    activeIndex,
    onNavigate: setActiveIndex,
  });
 
  const {getReferenceProps, getFloatingProps, getItemProps} =
    useInteractions([listNavigation]);
 
  const items = ['one', 'two', 'three'];
 
  return (
    <>
      <div ref={refs.setReference} {...getReferenceProps()}>
        Reference element
      </div>
      <div
        ref={refs.setFloating}
        style={floatingStyles}
        {...getFloatingProps()}
      >
        {items.map((item, index) => (
          <div
            key={item}
            // Make these elements focusable using a roving tabIndex.
            tabIndex={activeIndex === index ? 0 : -1}
            ref={(node) => {
              listRef.current[index] = node;
            }}
            {...getItemProps()}
          >
            {item}
          </div>
        ))}
      </div>
    </>
  );
}
function App() {
  const [activeIndex, setActiveIndex] = useState(null);
 
  const {refs, floatingStyles, context} = useFloating({
    open: true,
  });
 
  const listRef = useRef([]);
 
  const listNavigation = useListNavigation(context, {
    listRef,
    activeIndex,
    onNavigate: setActiveIndex,
  });
 
  const {getReferenceProps, getFloatingProps, getItemProps} =
    useInteractions([listNavigation]);
 
  const items = ['one', 'two', 'three'];
 
  return (
    <>
      <div ref={refs.setReference} {...getReferenceProps()}>
        Reference element
      </div>
      <div
        ref={refs.setFloating}
        style={floatingStyles}
        {...getFloatingProps()}
      >
        {items.map((item, index) => (
          <div
            key={item}
            // Make these elements focusable using a roving tabIndex.
            tabIndex={activeIndex === index ? 0 : -1}
            ref={(node) => {
              listRef.current[index] = node;
            }}
            {...getItemProps()}
          >
            {item}
          </div>
        ))}
      </div>
    </>
  );
}

Examples

Using with FloatingFocusManager

useListNavigation()useListNavigation() and <FloatingFocusManager><FloatingFocusManager> both manage focus but in different ways. To ensure they work together properly, the initial point of focus needs to be considered.

The focus manager by default focuses the first tabbable (not focusable) element inside of the floating element. If none are, it falls back to the floating element itself. This allows keydown events to work for pointer input (e.g. open with mouse, then start navigating with arrow keys).

  1. For a combobox, where the input should keep focus, set it to -1-1 so focus doesn’t move at all:
<FloatingFocusManager context={context} initialFocus={-1}>
  {/* floating element */}
</FloatingFocusManager>
<FloatingFocusManager context={context} initialFocus={-1}>
  {/* floating element */}
</FloatingFocusManager>
  1. For other types of components, like a Menu or a Select — where you want focus to move inside the floating element — the default value works, but make sure your list items aren’t tabbable if the activeIndex is nullnull.
// Not focusable, not tabbable.
<div />
// Tabbable and focusable.
<div tabIndex={0} />
// Not tabbable, but focusable.
<div tabIndex={-1} />
// Not focusable, not tabbable.
<div />
// Tabbable and focusable.
<div tabIndex={0} />
// Not tabbable, but focusable.
<div tabIndex={-1} />

A roving tabIndex is the recommended strategy:

<div tabIndex={activeIndex === index ? 0 : -1} />
<div tabIndex={activeIndex === index ? 0 : -1} />

Props

interface Props {
  listRef: React.MutableRefObject<Array<HTMLElement | null>>;
  activeIndex: number | null;
  onNavigate?: (index: number | null) => void;
  enabled?: boolean;
  selectedIndex?: number | null;
  loop?: boolean;
  nested?: boolean;
  rtl?: boolean;
  virtual?: boolean;
  allowEscape?: boolean;
  orientation?: 'vertical' | 'horizontal' | 'both';
  cols?: number;
  focusItemOnOpen?: 'auto' | boolean;
  focusItemOnHover?: boolean;
  openOnArrowKeyDown?: boolean;
  disabledIndices?: Array<number>;
  scrollItemIntoView?: boolean | ScrollIntoViewOptions;
}
interface Props {
  listRef: React.MutableRefObject<Array<HTMLElement | null>>;
  activeIndex: number | null;
  onNavigate?: (index: number | null) => void;
  enabled?: boolean;
  selectedIndex?: number | null;
  loop?: boolean;
  nested?: boolean;
  rtl?: boolean;
  virtual?: boolean;
  allowEscape?: boolean;
  orientation?: 'vertical' | 'horizontal' | 'both';
  cols?: number;
  focusItemOnOpen?: 'auto' | boolean;
  focusItemOnHover?: boolean;
  openOnArrowKeyDown?: boolean;
  disabledIndices?: Array<number>;
  scrollItemIntoView?: boolean | ScrollIntoViewOptions;
}

listRef

Required

default: empty list

A ref that holds an array of list items. You can assign each item in the array by its index like so:

const options = ['one', 'two', 'three'];
const listRef = useRef([]);
 
return options.map((option, index) => (
  <li
    key={option}
    ref={(node) => {
      listRef.current[index] = node;
    }}
  >
    {option}
  </li>
));
const options = ['one', 'two', 'three'];
const listRef = useRef([]);
 
return options.map((option, index) => (
  <li
    key={option}
    ref={(node) => {
      listRef.current[index] = node;
    }}
  >
    {option}
  </li>
));

activeIndex

Required

default: nullnull

The currently active (i.e. highlighted or focused) item index, which may or may not be selected.

const [activeIndex, setActiveIndex] = useState(null);
 
useListNavigation(context, {
  activeIndex,
});
const [activeIndex, setActiveIndex] = useState(null);
 
useListNavigation(context, {
  activeIndex,
});

onNavigate

default: no-op

Callback invoked when the user navigates, passed in the current activeIndexactiveIndex.

const [activeIndex, setActiveIndex] = useState(null);
 
useListNavigation(context, {
  onNavigate: setActiveIndex,
});
const [activeIndex, setActiveIndex] = useState(null);
 
useListNavigation(context, {
  onNavigate: setActiveIndex,
});

enabled

default: truetrue

Conditionally enable/disable the hook.

useListNavigation(context, {
  enabled: false,
});
useListNavigation(context, {
  enabled: false,
});

selectedIndex

default: nullnull

The currently selected item index, which may or may not be active.

This is the item shown in the trigger button/input.

const [selectedIndex, setSelectedIndex] = useState(null);
 
useListNavigation(context, {
  selectedIndex,
});
const [selectedIndex, setSelectedIndex] = useState(null);
 
useListNavigation(context, {
  selectedIndex,
});

loop

default: falsefalse

Whether to restart from the beginning or end if the user has navigated to the boundary of the list.

useListNavigation(context, {
  loop: true,
});
useListNavigation(context, {
  loop: true,
});

nested

default: falsefalse

If the list is nested within another one (e.g. a nested submenu), the navigation semantics change.

useListNavigation(context, {
  nested: true,
});
useListNavigation(context, {
  nested: true,
});

rtl

default: falsefalse

Whether the direction of the floating element’s navigation is in RTL layout.

useListNavigation(context, {
  rtl: true,
});
useListNavigation(context, {
  rtl: true,
});

virtual

default: falsefalse

Whether the focus is virtual (using aria-activedescendantaria-activedescendant).

Use this if you need focus to remain on the reference element (such as an input), but allow arrow keys to navigate list items. This is common in autocomplete listbox components.

useListNavigation(context, {
  virtual: true,
});
useListNavigation(context, {
  virtual: true,
});

allowEscape

Determines whether focus can escape the list, such that nothing is selected after navigating beyond the boundary of the list. In some autocomplete/combobox components, this may be desired, as screen readers will return to the input.

useListNavigation(context, {
  loop: true,
  allowEscape: true,
});
useListNavigation(context, {
  loop: true,
  allowEscape: true,
});

orientation

default: 'vertical''vertical'

The orientation in which navigation occurs.

useListNavigation(context, {
  orientation: 'horizontal',
});
useListNavigation(context, {
  orientation: 'horizontal',
});

cols

default: 11

Specifies how many columns the list has (i.e., it’s a grid).

Use an orientation of 'horizontal''horizontal' (e.g. for an emoji picker/date picker, where pressing ArrowRight or ArrowLeft can change rows), or 'both''both' (where the current row cannot be escaped with ArrowRight or ArrowLeft, only ArrowUp and ArrowDown).

useListNavigation(context, {
  // 4 columns, any number of rows
  cols: 4,
});
useListNavigation(context, {
  // 4 columns, any number of rows
  cols: 4,
});

focusItemOnOpen

default: 'auto''auto'

Whether to focus the item upon opening the floating element. 'auto''auto' infers what to do based on the input type (keyboard vs. pointer), while a boolean value will force the value.

useListNavigation(context, {
  focusItemOnOpen: true,
});
useListNavigation(context, {
  focusItemOnOpen: true,
});

focusItemOnHover

default: truetrue

Whether hovering an item synchronizes the focus.

useListNavigation(context, {
  focusItemOnHover: false,
});
useListNavigation(context, {
  focusItemOnHover: false,
});

openOnArrowKeyDown

default: truetrue

Whether pressing an arrow key on the navigation’s main axis opens the floating element.

useListNavigation(context, {
  openOnArrowKeyDown: false,
});
useListNavigation(context, {
  openOnArrowKeyDown: false,
});

disabledIndices

default: undefinedundefined

By default elements with either a disableddisabled or aria-disabledaria-disabled attribute are skipped in the list navigation – however, this requires the items to be rendered.

This prop allows you to manually specify indices which should be disabled, overriding the default logic.

For Windows-style select menus, where the menu does not open when navigating via arrow keys, specify an empty array.

useListNavigation(context, {
  disabledIndices: [0, 3],
});
useListNavigation(context, {
  disabledIndices: [0, 3],
});

scrollItemIntoView

default: truetrue

Whether to scroll the active item into view when navigating. The default value uses nearestnearest options.

useListNavigation(context, {
  scrollItemIntoView: false,
});
useListNavigation(context, {
  scrollItemIntoView: false,
});