Autocomplete

Low-level primitive for an autocomplete input that suggests options as you type. Unlike Combobox, the input can contain free-form text and suggestions only optionally autocomplete the text.

There are currently no plans for higher-level components of this primitive in this package. Use the primitives directly, and remember to label the input field, usually by using the Field component.

import { Autocomplete } from '@wordpress/ui';

View on Storybook

View source on GitHub

PropsPermalink to this section

NameDefaultDescription
form

string

Identifies the form that owns the internal input. Useful when the autocomplete is rendered outside the form.

items

readonly unknown[] | readonly Group<unknown>[] | undefined

The items to be displayed in the list. Can be either a flat array of items or an array of groups with items. Nullish entries are not supported: remove them from the data before passing it.

filteredItems

readonly unknown[] | readonly Group<unknown>[] | undefined

Filtered items to display in the list. When provided, the list uses these items instead of filtering the items prop internally. When items is also provided, this array must preserve its flat or grouped structure. Nullish entries are not supported, as in items. Use when you want to control filtering logic externally with the useFilter() hook.

filter

((item: unknown, query: string, itemToString?: ((item: unknown) => string) | undefined) => boolean) | null | undefined

Filter function used to match items against the input query.

mode'list'

"none" | "list" | "inline" | "both"

Controls how the autocomplete behaves with respect to list filtering and inline autocompletion.

  • list (default): items are dynamically filtered based on the input value. The input value does not change based on the active item.
  • both: items are dynamically filtered based on the input value, which will temporarily change based on the active item (inline autocompletion).
  • inline: items are static (not filtered), and the input value will temporarily change based on the active item (inline autocompletion).
  • none: items are static (not filtered), and the input value will not change based on the active item.
inlinefalse

boolean

Whether the list is rendered inline without using the component’s own popup.

Specify open unconditionally in conjunction with this prop so the list is considered visible: <Autocomplete.Root inline open>

autoHighlightfalse

boolean | "always" | undefined

Whether the first matching item is highlighted automatically.

  • true: highlight after the user types and keep the highlight while the query changes.
  • 'always': always highlight the first item.
keepHighlightfalse

boolean

Whether the highlighted item should be preserved when the pointer leaves the list.

highlightItemOnHovertrue

boolean

Whether moving the pointer over items should highlight them. Disabling this prop allows CSS :hover to be differentiated from the :focus (data-highlighted) state.

defaultValue

string | number | readonly string[] | undefined

The uncontrolled input value of the autocomplete when it’s initially rendered.

To render a controlled autocomplete, use the value prop instead.

value

string | number | readonly string[] | undefined

The input value of the autocomplete. Use when controlled.

onValueChange

(value: string, eventDetails: AutocompleteRootChangeEventDetails) => void

Event handler called when the input value of the autocomplete changes.

submitOnItemClickfalse

boolean

Whether clicking an item should submit the autocomplete’s owning form. By default, clicking an item via a pointer or <kbd>Enter</kbd> key does not submit the owning form. Useful when the autocomplete is used as a single-field form search input.

itemToStringValue

(itemValue: unknown) => string

When the item values are objects (<Autocomplete.Item value={object}>), this function converts the object value to a string representation for both display in the input and form submission. If the shape of the object is { value, label }, the label will be used automatically without needing to specify this prop.

actionsRef

RefObject<AutocompleteRootActions | null>

A ref to imperative actions.

  • unmount: Manually unmounts the autocomplete. Call this after any externally controlled closing animation finishes.
onOpenChange

(open: boolean, eventDetails: AutocompleteRootChangeEventDetails) => void

Event handler called when the popup is opened or closed.

onItemHighlighted

(highlightedValue: unknown, eventDetails: HighlightEventDetails) => void

Callback fired when an item is highlighted or unhighlighted. Receives the highlighted item value (or undefined if no item is highlighted) and event details with a reason property describing why the highlight changed. The reason can be:

  • 'keyboard': the highlight changed due to keyboard navigation.
  • 'pointer': the highlight changed due to pointer hovering.
  • 'none': the highlight changed programmatically.
openOnInputClickfalse

boolean

Whether the popup opens when clicking the input.

children

ReactNode

disabledfalse

boolean

Whether the component should ignore user interaction.

name

string

Identifies the field when a form is submitted.

id

string

The id of the component.

gridfalse

boolean

Whether list items are presented in a grid layout. When enabled, arrow keys navigate across rows and columns inferred from DOM rows.

open

boolean

Whether the popup is currently open. Use when controlled.

readOnlyfalse

boolean

Whether the user should be unable to choose a different option from the popup.

requiredfalse

boolean

Whether the user must choose a value before submitting a form.

defaultOpenfalse

boolean

Whether the popup is initially open.

To render a controlled popup, use the open prop instead.

limit-1

number

The maximum number of items to display in the list.

locale

LocalesArgument

The locale to use for string comparison. Defaults to the user’s runtime locale.

onOpenChangeComplete

(open: boolean) => void

Event handler called after any animations complete when the popup is opened or closed.

loopFocustrue

boolean

Whether to loop keyboard focus back to the input when the end of the list is reached while using the arrow keys. The first item can then be reached by pressing <kbd>ArrowDown</kbd> again from the input, or the last item can be reached by pressing <kbd>ArrowUp</kbd> from the input. The input is always included in the focus loop per ARIA Authoring Practices. When disabled, focus does not move when on the last element and the user presses <kbd>ArrowDown</kbd>, or when on the first element and the user presses <kbd>ArrowUp</kbd>.

inputRef

Ref<HTMLInputElement> | undefined

A ref to the hidden input element.

virtualizedfalse

boolean

Whether the items are being externally virtualized.

modalfalse

boolean

Determines if the popup enters a modal state when open.

  • true: user interaction is limited to the popup: document page scroll is locked and pointer interactions on outside elements are disabled.
  • false: user interaction with the rest of the document is allowed.

On touch devices, a true modal blocks outside taps but leaves the page scrollable unless the popup spans nearly the full viewport width, matching native iOS behavior.

ExamplesPermalink to this section

DefaultPermalink to this section

The input accepts free-form text while suggesting matching items.

const Default = () => <Autocomplete.Root items={URLS}>{[
        <Autocomplete.Input
            aria-label="URL"
            placeholder="Enter a URL"
            key="input"
        />,
        <Autocomplete.Popup key="popup">
            <Autocomplete.Empty>No matching items.</Autocomplete.Empty>
            <Autocomplete.List>
                <Autocomplete.ListBody>
                    <Autocomplete.Collection>
                        { ( item: FixtureItem ) => (
                            <Autocomplete.Item
                                key={ item.id }
                                value={ item }
                            >
                                { item.value }
                            </Autocomplete.Item>
                        ) }
                    </Autocomplete.Collection>
                </Autocomplete.ListBody>
            </Autocomplete.List>
        </Autocomplete.Popup>,
    ]}</Autocomplete.Root>;

Open Only On MatchPermalink to this section

Controls the popup so it only opens when there is at least one match.

const OpenOnlyOnMatch = () => {
    const [ open, setOpen ] = useState( false );
    const [ filteredItems, setFilteredItems ] = useState( URLS );

    return (
        <Autocomplete.Root
            items={ URLS }
            open={ open }
            onOpenChange={ ( nextOpen ) => {
                setOpen( nextOpen && filteredItems.length > 0 );
            } }
            onValueChange={ ( value ) => {
                const matches = URLS.filter( ( bookmark ) =>
                    bookmark.value
                        .toLowerCase()
                        .includes( value.toLowerCase() )
                );
                setFilteredItems( matches );
                setOpen( value.length > 0 && matches.length > 0 );
            } }
            filteredItems={ filteredItems }>
            <Autocomplete.Input aria-label="URL" placeholder="Enter a URL" />
            <Autocomplete.Popup>
                <Autocomplete.List>
                    <Autocomplete.ListBody>
                        <Autocomplete.Collection>
                            { ( item: FixtureItem ) => (
                                <Autocomplete.Item
                                    key={ item.id }
                                    value={ item }
                                >
                                    { item.value }
                                </Autocomplete.Item>
                            ) }
                        </Autocomplete.Collection>
                    </Autocomplete.ListBody>
                </Autocomplete.List>
            </Autocomplete.Popup>
        </Autocomplete.Root>
    );
};

Async ItemsPermalink to this section

Fetches matching items asynchronously. Status shows loading, then a visually hidden result count. Use Empty for no results.

const AsyncItems = () => {
    const [ query, setQuery ] = useState( '' );
    const [ loading, setLoading ] = useState( false );
    const [ results, setResults ] = useState< typeof URLS >( [] );
    const timeoutRef = useRef< ReturnType< typeof setTimeout > >();

    return (
        <Autocomplete.Root
            items={ results }
            value={ query }
            onValueChange={ ( newValue ) => {
                setQuery( newValue );
                setLoading( true );
                setResults( [] );
                clearTimeout( timeoutRef.current );
                timeoutRef.current = setTimeout( () => {
                    setResults(
                        URLS.filter( ( item ) =>
                            item.value
                                .toLowerCase()
                                .includes( newValue.toLowerCase() )
                        )
                    );
                    setLoading( false );
                }, 500 );
            } }>
            <Autocomplete.Input aria-label="URL" placeholder="Enter a URL" />
            <Autocomplete.Popup>
                <Autocomplete.Status>
                    { loading ? (
                        <Stack direction="row" gap="sm" align="center">
                            <Spinner />
                            Loading…
                        </Stack>
                    ) : (
                        <HiddenResultCount />
                    ) }
                </Autocomplete.Status>
                <Autocomplete.Empty>
                    { loading ? null : 'No matching items.' }
                </Autocomplete.Empty>
                <Autocomplete.List>
                    <Autocomplete.ListBody>
                        <Autocomplete.Collection>
                            { ( item: FixtureItem ) => (
                                <Autocomplete.Item
                                    key={ item.id }
                                    value={ item }
                                >
                                    { item.value }
                                </Autocomplete.Item>
                            ) }
                        </Autocomplete.Collection>
                    </Autocomplete.ListBody>
                </Autocomplete.List>
            </Autocomplete.Popup>
        </Autocomplete.Root>
    );
};

InlinePermalink to this section

The suggestion list can be rendered inline by enabling inline and open.

const Inline = () => {
    const [ value, setValue ] = useState( '' );

    return (
        <Autocomplete.Root items={COMMANDS} inline open value={ value } onValueChange={ setValue }>
            <Autocomplete.Input aria-label="Command" placeholder="Type a command" />
            <div
                style={ {
                    minHeight: '200px',
                    maxHeight: '200px',
                    marginTop: 8,
                    overflow: 'auto',
                } }>
                <Autocomplete.Empty>No commands found.</Autocomplete.Empty>
                <Autocomplete.List>
                    <Autocomplete.Collection>
                        { ( command: FixtureItem ) => (
                            <Autocomplete.Item
                                key={ command.id }
                                value={ command }
                            >
                                { command.value }
                            </Autocomplete.Item>
                        ) }
                    </Autocomplete.Collection>
                </Autocomplete.List>
            </div>
        </Autocomplete.Root>
    );
};

With Search Icon And Clear ButtonPermalink to this section

const WithSearchIconAndClearButton = () => <Autocomplete.Root items={URLS}>{[
        <Autocomplete.InputGroup key="inputGroup">
            <Autocomplete.Input
                aria-label="Search URLs"
                placeholder="Search URLs"
                render={
                    <Input
                        prefix={
                            <InputLayout.Slot padding="minimal">
                                <Icon icon={ search } />
                            </InputLayout.Slot>
                        }
                        suffix={
                            <InputLayout.Slot padding="minimal">
                                <Autocomplete.Clear />
                            </InputLayout.Slot>
                        }
                    />
                }
            />
        </Autocomplete.InputGroup>,
        <Autocomplete.Popup key="popup">
            <Autocomplete.Empty>No matching items.</Autocomplete.Empty>
            <Autocomplete.List>
                <Autocomplete.ListBody>
                    <Autocomplete.Collection>
                        { ( item: FixtureItem ) => (
                            <Autocomplete.Item
                                key={ item.id }
                                value={ item }
                            >
                                { item.value }
                            </Autocomplete.Item>
                        ) }
                    </Autocomplete.Collection>
                </Autocomplete.ListBody>
            </Autocomplete.List>
        </Autocomplete.Popup>,
    ]}</Autocomplete.Root>;

Inline Mention AutocompletePermalink to this section

Experimental: Inline autocomplete triggered by @.

const InlineMentionAutocomplete = function Template() {
    const inputRef = useRef< HTMLInputElement >( null );
    const [ value, setValue ] = useState( '' );
    const [ open, setOpen ] = useState( false );
    const [ filteredItems, setFilteredItems ] = useState< FixtureItem[] >(
        []
    );
    const triggerInfo = useRef< {
        offset: number;
        query: string;
    } | null >( null );

    function findTrigger( text: string, caretPos: number ) {
        const textBeforeCaret = text.slice( 0, caretPos );
        const triggerIndex = textBeforeCaret.lastIndexOf( '@' );
        if ( triggerIndex < 0 ) {
            return null;
        }

        const query = textBeforeCaret.slice( triggerIndex + 1 );
        if ( /\s/.test( query ) ) {
            return null;
        }
        if ( triggerIndex > 0 && ! /\s/.test( text[ triggerIndex - 1 ] ) ) {
            return null;
        }

        return { offset: triggerIndex, query };
    }

    function handleValueChange(
        newValue: string,
        details: { reason: string }
    ) {
        const trigger = triggerInfo.current;

        if ( details.reason === 'item-press' && trigger ) {
            const before = value.slice( 0, trigger.offset );
            const afterEndPos = trigger.offset + 1 + trigger.query.length;
            const after = value.slice( afterEndPos );
            const needsSpace = after.length > 0 && after[ 0 ] !== ' ';
            const inserted = `@${ newValue }${ needsSpace ? ' ' : '' }`;
            const fullValue = before + inserted + after;

            setValue( fullValue );
            triggerInfo.current = null;
            setOpen( false );

            const caretPos = before.length + inserted.length;
            requestAnimationFrame( () => {
                inputRef.current?.setSelectionRange( caretPos, caretPos );
                inputRef.current?.focus();
            } );
            return;
        }

        setValue( newValue );

        const input = inputRef.current;
        if ( ! input ) {
            return;
        }

        const caretPos = input.selectionStart ?? 0;
        const detected = findTrigger( newValue, caretPos );

        if ( detected ) {
            triggerInfo.current = detected;
            const matches = USERS.filter( ( user ) =>
                user.value
                    .toLowerCase()
                    .startsWith( detected.query.toLowerCase() )
            );
            setFilteredItems( matches );
            setOpen( matches.length > 0 );
        } else {
            triggerInfo.current = null;
            setOpen( false );
        }
    }

    return (
        <Autocomplete.Root
            items={ USERS }
            value={ value }
            onValueChange={ handleValueChange }
            filteredItems={ filteredItems }
            open={ open }
            onOpenChange={ ( nextOpen ) => {
                if ( ! nextOpen ) {
                    setOpen( false );
                    triggerInfo.current = null;
                }
            } }
            mode="none"
            openOnInputClick={ false }
            autoHighlight
        >
            <Autocomplete.Input
                ref={ inputRef }
                aria-label="Comment"
                placeholder="Type @ to mention someone"
            />

            <Autocomplete.Popup>
                <Autocomplete.List>
                    <Autocomplete.ListBody>
                        <Autocomplete.Collection>
                            { ( item: FixtureItem ) => (
                                <Autocomplete.Item
                                    key={ item.id }
                                    value={ item }
                                >
                                    { item.value }
                                </Autocomplete.Item>
                            ) }
                        </Autocomplete.Collection>
                    </Autocomplete.ListBody>
                </Autocomplete.List>
            </Autocomplete.Popup>
        </Autocomplete.Root>
    );
};

With Custom z-indexPermalink to this section

Popovers in Gutenberg are managed with explicit z-index values, which can create situations where an autocomplete popup renders below another popover when you want it above.

The --wp-ui-autocomplete-z-index CSS variable controls the z-index of the Autocomplete positioner. Override it either:

  • Globally, by setting the variable on :root or body (raises every Autocomplete popup in the page), or
  • Per instance, by passing an Autocomplete.Portal with a style (or className) to Autocomplete.Popup‘s portal prop. The variable cascades from the portal wrapper to everything rendered inside it.

This story demonstrates the per-instance approach.

const WithCustomZIndex = () => <Autocomplete.Root items={URLS}>{[
        <Autocomplete.Input
            aria-label="URL"
            placeholder="Enter a URL"
            key="input"
        />,
        <Autocomplete.Popup
            portal={
                <Autocomplete.Portal
                    style={ { '--wp-ui-autocomplete-z-index': '9999' } }
                />
            }
            key="popup"
        >
            <Autocomplete.List>
                <Autocomplete.ListBody>
                    <Autocomplete.Collection>
                        { ( item: FixtureItem ) => (
                            <Autocomplete.Item
                                key={ item.id }
                                value={ item }
                            >
                                { item.value }
                            </Autocomplete.Item>
                        ) }
                    </Autocomplete.Collection>
                </Autocomplete.ListBody>
            </Autocomplete.List>
        </Autocomplete.Popup>,
    ]}</Autocomplete.Root>;

GroupedPermalink to this section

Suggestions can be organized into labeled groups with Autocomplete.Group and Autocomplete.GroupLabel.

const Grouped = () => <Autocomplete.Root items={GROUPED_COMMANDS}>{[
        <Autocomplete.Input
            aria-label="Command"
            placeholder="Type a command"
            key="input"
        />,
        <Autocomplete.Popup key="popup">
            <Autocomplete.Empty>No matching items.</Autocomplete.Empty>
            <Autocomplete.List>
                <Autocomplete.ListBody>
                    <Autocomplete.Collection>
                        { ( group: FixtureGroup ) => (
                            <Autocomplete.Group
                                key={ group.label }
                                items={ group.items }
                            >
                                <Autocomplete.GroupLabel>
                                    { group.label }
                                </Autocomplete.GroupLabel>
                                <Autocomplete.Collection>
                                    { ( item: FixtureItem ) => (
                                        <Autocomplete.Item
                                            key={ item.id }
                                            value={ item }
                                        >
                                            { item.value }
                                        </Autocomplete.Item>
                                    ) }
                                </Autocomplete.Collection>
                            </Autocomplete.Group>
                        ) }
                    </Autocomplete.Collection>
                </Autocomplete.ListBody>
            </Autocomplete.List>
        </Autocomplete.Popup>,
    ]}</Autocomplete.Root>;

GridPermalink to this section

Autocomplete.Row groups multiple Autocomplete.Item cells into grid rows. Enable grid on Autocomplete.Root so the listbox uses grid navigation.

const Grid = () => {
    return (
        <Autocomplete.Root items={EMOJI_GROUPS} inline open grid>
            <Autocomplete.Input aria-label="Search emojis" placeholder="Search emojis" />
            <div
                style={ {
                    marginTop: 'var(--wpds-dimension-gap-sm)',
                } }>
                <Autocomplete.Empty>No matching emojis.</Autocomplete.Empty>
                <Autocomplete.List>
                    { ( group: ( typeof EMOJI_GROUPS )[ number ] ) => (
                        <Autocomplete.Group
                            key={ group.value }
                            items={ group.items }
                        >
                            <Autocomplete.GroupLabel>
                                { group.label }
                            </Autocomplete.GroupLabel>
                            { chunkItems( group.items, EMOJI_COLUMNS ).map(
                                ( row, rowIndex ) => (
                                    <Autocomplete.Row
                                        key={ rowIndex }
                                        style={ emojiPickerRowStyle }
                                    >
                                        { row.map( ( emoji ) => (
                                            <Autocomplete.Item
                                                key={ emoji.value }
                                                value={ emoji }
                                                aria-label={ emoji.label }
                                                style={
                                                    emojiPickerCellStyle
                                                }
                                            >
                                                <span aria-hidden="true">
                                                    { emoji.emoji }
                                                </span>
                                            </Autocomplete.Item>
                                        ) ) }
                                    </Autocomplete.Row>
                                )
                            ) }
                        </Autocomplete.Group>
                    ) }
                </Autocomplete.List>
            </div>
        </Autocomplete.Root>
    );
};