Skip to content

useTypeahead

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

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

See FloatingList for creating composable children API components.

Usage

This hook is an interaction hook that returns event handler 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.

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

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,
});
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([]);
// 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;
  }}
/>
<li
  ref={(node) => {
    listItemsRef.current[index] = node;
    listContentRef.current[index] = node?.textContent ?? null;
  }}
/>

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

activeIndex

Required

default: nullnull

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

const [activeIndex, setActiveIndex] = useState(null);
 
useTypeahead(context, {
  activeIndex,
});
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,
});
const [isOpen, setIsOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(null);
const [selectedIndex, setSelectedIndex] = useState(null);
 
useTypeahead(context, {
  onMatch: isOpen ? setActiveIndex : setSelectedIndex,
});

enabled

default: truetrue

Conditionally enable/disable the hook.

useTypeahead(context, {
  enabled: false,
});
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
    ),
});
useTypeahead(context, {
  findMatch: (list, typedString) =>
    list.find(
      (itemString) =>
        itemString?.toLowerCase().indexOf(typedString) === 0
    ),
});

resetMs

default: 750750

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

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

ignoreKeys

default: [][]

Optional keys to ignore.

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

selectedIndex

default: nullnull

The currently selected index, if available.

const [selectedIndex, setSelectedIndex] = useState(null);
 
useTypeahead(context, {
  selectedIndex,
});
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) {
    // ...
  },
});
useTypeahead(context, {
  onTypingChange(isTyping) {
    // ...
  },
});