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';
LinksPermalink to this section
PropsPermalink to this section
| Name | Default | Description |
|---|---|---|
form |
Identifies the form that owns the internal input. Useful when the autocomplete is rendered outside the form. | |
items |
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 |
Filtered items to display in the list.
When provided, the list uses these items instead of filtering the | |
filter |
Filter function used to match items against the input query. | |
mode | 'list' |
Controls how the autocomplete behaves with respect to list filtering and inline autocompletion.
|
inline | false |
Whether the list is rendered inline without using the component’s own popup. Specify |
autoHighlight | false |
Whether the first matching item is highlighted automatically.
|
keepHighlight | false |
Whether the highlighted item should be preserved when the pointer leaves the list. |
highlightItemOnHover | true |
Whether moving the pointer over items should highlight them.
Disabling this prop allows CSS |
defaultValue |
The uncontrolled input value of the autocomplete when it’s initially rendered. To render a controlled autocomplete, use the | |
value |
The input value of the autocomplete. Use when controlled. | |
onValueChange |
Event handler called when the input value of the autocomplete changes. | |
submitOnItemClick | false |
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 |
When the item values are objects ( | |
actionsRef |
A ref to imperative actions.
| |
onOpenChange |
Event handler called when the popup is opened or closed. | |
onItemHighlighted |
Callback fired when an item is highlighted or unhighlighted.
Receives the highlighted item value (or
| |
openOnInputClick | false |
Whether the popup opens when clicking the input. |
children |
| |
disabled | false |
Whether the component should ignore user interaction. |
name |
Identifies the field when a form is submitted. | |
id |
The id of the component. | |
grid | false |
Whether list items are presented in a grid layout. When enabled, arrow keys navigate across rows and columns inferred from DOM rows. |
open |
Whether the popup is currently open. Use when controlled. | |
readOnly | false |
Whether the user should be unable to choose a different option from the popup. |
required | false |
Whether the user must choose a value before submitting a form. |
defaultOpen | false |
Whether the popup is initially open. To render a controlled popup, use the |
limit | -1 |
The maximum number of items to display in the list. |
locale |
The locale to use for string comparison. Defaults to the user’s runtime locale. | |
onOpenChangeComplete |
Event handler called after any animations complete when the popup is opened or closed. | |
loopFocus | true |
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 |
A ref to the hidden input element. | |
virtualized | false |
Whether the items are being externally virtualized. |
modal | false |
Determines if the popup enters a modal state when open.
On touch devices, a |
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
:rootorbody(raises everyAutocompletepopup in the page), or - Per instance, by passing an
Autocomplete.Portalwith astyle(orclassName) toAutocomplete.Popup‘sportalprop. 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>
);
};