Skip to content

useTypeahead

Provides a matching callback that can be used to focus an item as the user types, used in tandem with useListNavigation().

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

This is useful for creating a menu with typeahead support, where the user can type to focus an item and then immediately select it, especially if it contains a large number of items.

See FloatingList for creating composable children API components.

Usage

This Hook returns event handler props.

To use it, pass it the context object returned from useFloating(), and then feed its result into the useInteractions() array. The returned prop getters are then spread onto the elements for rendering.

useListNavigation() is responsible for synchronizing the index for focus.

function App() {
  const [activeIndex, setActiveIndex] = useState(null);
 
  const {refs, floatingStyles, context} = useFloating({
    open: true,
  });
 
  const items = ['one', 'two', 'three'];
 
  const listRef = useRef(items);
 
  const typeahead = useTypeahead(context, {
    listRef,
    activeIndex,
    onMatch: setActiveIndex,
  });
 
  const {getReferenceProps, getFloatingProps, getItemProps} =
    useInteractions([typeahead]);
 
  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}
            {...getItemProps()}
          >
            {item}
          </div>
        ))}
      </div>
    </>
  );
}

Props

interface UseTypeaheadProps {
  listRef: React.MutableRefObject<Array<string | null>>;
  activeIndex: number | null;
  onMatch?(index: number): void;
  enabled?: boolean;
  resetMs?: number;
  ignoreKeys?: Array<string>;
  selectedIndex?: number | null;
  onTypingChange?(isTyping: boolean): void;
  findMatch?:
    | null
    | ((
        list: Array<string | null>,
        typedString: string,
      ) => string | null | undefined);
}

listRef

Required

default: empty list

A ref which contains an array of strings whose indices match the HTML elements of the list.

const listRef = useRef(['one', 'two', 'three']);
 
useTypeahead(context, {
  listRef,
});

You can derive these strings when assigning the node if the strings are not available up front:

// Array<HTMLElement | null> for `useListNavigation`
const listItemsRef = useRef([]);
// Array<string | null> for `useTypeahead`
const listContentRef = useRef([]);
<li
  ref={(node) => {
    listItemsRef.current[index] = node;
    listContentRef.current[index] = node?.textContent ?? null;
  }}
/>

Disabled items can be represented by null values in the array at the relevant index, and will be skipped.

activeIndex

Required

default: null

The currently active index. This specifies where the typeahead starts.

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

onMatch

default: no-op

Callback invoked with the matching index if found as the user types.

const [isOpen, setIsOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(null);
const [selectedIndex, setSelectedIndex] = useState(null);
 
useTypeahead(context, {
  onMatch: isOpen ? setActiveIndex : setSelectedIndex,
});

enabled

default: true

Conditionally enable/disable the Hook.

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

findMatch

default: lowercase finder

If you’d like to implement custom finding logic (for example fuzzy search), you can use this callback.

useTypeahead(context, {
  findMatch: (list, typedString) =>
    list.find(
      (itemString) =>
        itemString?.toLowerCase().indexOf(typedString) === 0,
    ),
});

resetMs

default: 750

Debounce timeout which will reset the transient string as the user types.

useTypeahead(context, {
  resetMs: 500,
});

ignoreKeys

default: []

Optional keys to ignore.

useTypeahead(context, {
  ignoreKeys: ['I', 'G', 'N', 'O', 'R', 'E'],
});

selectedIndex

default: null

The currently selected index, if available.

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

onTypingChange

default: no-op

Callback invoked with the typing state as the user types.

useTypeahead(context, {
  onTypingChange(isTyping) {
    // ...
  },
});