BetaForms

Autocomplete

A field that opens a list you search.

Overview

A field that opens a list you search.

First example

<Autocomplete onValueChange={setState}>
<Autocomplete.Label>État</Autocomplete.Label>
<Autocomplete.Trigger>
<Autocomplete.Value placeholder="Choisir un état" />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Overlay />
<Autocomplete.Content>
<Autocomplete.Search placeholder="Rechercher…" />
<Autocomplete.Item value="ca">Californie</Autocomplete.Item>
<Autocomplete.Item value="tx">Texas</Autocomplete.Item>
<Autocomplete.Empty>Aucun résultat</Autocomplete.Empty>
</Autocomplete.Content>
<Autocomplete.Description>
Cinquante États, une seule ligne.
</Autocomplete.Description>
</Autocomplete>

Anatomy

slotwhat it is
AutocompleteThe column, and the resolved style
Autocomplete.LabelWhat the field is for
Autocomplete.TriggerThe control — a combobox
Autocomplete.ValueThe chosen row's label, or the placeholder
Autocomplete.IndicatorThe chevron, turning with the panel
Autocomplete.OverlayThe backdrop. Optional
Autocomplete.ContentThe panel
Autocomplete.SearchThe field you type in
Autocomplete.ItemOne result
Autocomplete.EmptyWhat the panel says when nothing matches
Autocomplete.DescriptionThe hint under the field
Autocomplete.ErrorWhat is wrong with the choice

Usage

<Autocomplete onValueChange={setState}>
  <Autocomplete.Label>État</Autocomplete.Label>
  <Autocomplete.Trigger>
    <Autocomplete.Value placeholder="Choisir un état" />
    <Autocomplete.Indicator />
  </Autocomplete.Trigger>
  <Autocomplete.Overlay />
  <Autocomplete.Content>
    <Autocomplete.Search placeholder="Rechercher…" />
    <Autocomplete.Item value="ca">Californie</Autocomplete.Item>
    <Autocomplete.Item value="tx">Texas</Autocomplete.Item>
    <Autocomplete.Empty>Aucun résultat</Autocomplete.Empty>
  </Autocomplete.Content>
  <Autocomplete.Description>
    Cinquante États, une seule ligne.
  </Autocomplete.Description>
</Autocomplete>

Root props

AutocompleteProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo
variantAutocompleteVariant | undefinedNo
sizeAutocompleteSize | undefinedNo
radiusRadiusKey | undefinedNo
colorstring | undefinedNoThe tint (R7) — a raw value, never a token.
valuestring | undefinedNoControlled selection. Leave unset and the root owns it.
defaultValuestring | undefinedNo
onValueChange((value: string) => void) | undefinedNo
isOpenboolean | undefinedNoControlled open state. Leave unset and the root owns it.
defaultOpenboolean | undefinedNo
onOpenChange((isOpen: boolean) => void) | undefinedNo
querystring | undefinedNoControlled query. Leave unset and the root owns it — and clears it every time the panel closes, because a search that survives its own panel means the list is already filtered the next time it opens, by a word nobody can see.
defaultQuerystring | undefinedNo
onQueryChange((query: string) => void) | undefinedNo
isDisabledboolean | undefinedNo
isInvalidboolean | undefinedNo
asChildboolean | undefinedNo

Slots

AutocompleteContentProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

This slot adds nothing to the props of its React Native node.

AutocompleteDescriptionProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

AutocompleteEmptyProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

AutocompleteErrorProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

AutocompleteItemLabelProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

AutocompleteItemProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
valuestringYes
labelstring | undefinedNoRead before the row mounts, for a trigger that has to name a value it has never shown.
childrenReactNodeNo
isDisabledboolean | undefinedNo
asChildboolean | undefinedNo

AutocompleteLabelProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

AutocompleteOverlayProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

This slot adds nothing to the props of its React Native node.

AutocompleteSearchProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
autoFocusboolean | undefinedNoWhether the field takes focus as the panel opens. On by default: an autocomplete that has to be tapped twice before it can be typed into is a select with a spare row.

AutocompleteTriggerProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo
asChildboolean | undefinedNo

AutocompleteValueProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo
placeholderstring | undefinedNo

Variants, sizes and colour

This component adds no visual axis of its own. The values it does take are in the generated types above and in the live demo.

Accessibility

The trigger is a combobox rather than a button — the control opens a list you type into, and that is the role that says so. It carries expanded and, when isInvalid, aria-invalid. The search field is a search. Both stay overridable (R9).

The trigger points at Autocomplete.Label and Autocomplete.Description through aria-labelledby and aria-describedby, which is what makes a screen reader announce "État, liste déroulante" rather than reading the chosen row and leaving the question unsaid. The ids come from the root, so mounting the two slots is all it takes; your own aria-labelledby on the trigger still wins.

The overlay announces nothing: it is the absence of the panel, and a screen reader saying "button" over the whole screen is worse than saying nothing.

Migration from legacy

This component declares no rule of its own. The migration guide covers variants, colours and slots.

Implementation notes

The root is the column, not the field

A View that stacks Autocomplete.Label, the trigger and Autocomplete.Description / .Error with one gap, so JSX order is screen order. It is the column and the help lines that make this a field on a form rather than a button that opens a list — and they are the TextField's, token for token, so a text field and an autocomplete on the same form read as one control. The DatePicker borrows the same three slots for the same reason.

isInvalid paints the trigger's border and turns the label and the description danger; it never mounts or unmounts Autocomplete.Error. A slot that silently renders nothing is one you cannot debug, so you write the condition yourself:

<Autocomplete isInvalid={Boolean(error)}>
  <Autocomplete.Label>État</Autocomplete.Label>
  <Autocomplete.Trigger>
    <Autocomplete.Value placeholder="Choisir un état" />
    <Autocomplete.Indicator />
  </Autocomplete.Trigger>
  …{error ? <Autocomplete.Error>{error}</Autocomplete.Error> : null}
</Autocomplete>

Autocomplete.Trigger stays the field: it keeps its own ref — it is the node the panel measures — and its own a11y props. Autocomplete.Overlay and Autocomplete.Content portal out, so they add nothing to the column.

It is not a `Select`

A select is for a list you read: a dozen options, all of them visible, and choosing is recognising one. An autocomplete is for a list you cannot read — fifty states, four thousand cities — where choosing is finding, and the field you type in is the control rather than an extra row in a menu.

Same trigger, same panel, same rows; a different thing to do with them. So the two share their style by construction: the trigger, the panel and the rows resolve through selectRecipe, and only the search box and the empty line are this component's own. A second table would be two to keep in step, and the drift would show as a select and an autocomplete side by side in a form with fields half a shade apart.

The rows are plainer than a select's for the same reason. No description, no check: a check beside the row already chosen is of no use in a list you reached by searching for something else.

The search is the control

Autocomplete.Search lives inside Autocomplete.Content, and the panel pins it above its scroller so it stays put while the results move under it. It takes focus as the panel opens — an autocomplete you have to tap twice before you can type into it is a select with a spare row — and autoFocus={false} turns that off for a panel short enough that the keyboard would cover it.

The query goes with the panel. Closing clears it, because a search that survived its own closing would mean the list is already filtered the next time it opens, by a word nobody can see. query / defaultQuery / onQueryChange if you would rather own it.

What matching means

geneve finds Genève, and genève finds Geneve — diacritics are folded and case is dropped, both ways.

york finds New York: it matches any word rather than the first, because a long list is searched by whichever word someone remembers, not by the one that happens to come first.

An empty query keeps everything, which is what makes the field's resting state the whole list rather than none of it.

Filtering is by row, and only the rows it can see

Autocomplete.Content drops the rows that do not survive the query. It reads them the way Autocomplete.Value reads the chosen label — off the elements, before any of them mounts — but only its direct children.

Walking deeper to read a label changes nothing; dropping a row nested inside a component of your own would mean rebuilding that component's children for it, and a filter that silently rewrote your tree is worse than one that leaves it alone. A row wrapped in something of yours is a row the search will not hide.

A row with no readable label is never hidden either — hiding it would make a custom row disappear the moment anyone typed.

`Autocomplete.Empty` renders instead of the results

And only when nothing matched. A panel that filtered its last row away and showed an empty box reads as a control that has broken rather than as a search that found nothing.