Select
A field that opens a list. It is the TextField's twin: the same field tokens, the same four levels, the same heights — so the two sit in a form and read as one control.
Overview
A field that opens a list. It is the TextField's twin: the same field* tokens, the
same four levels, the same heights — so the two sit in a form and read as one control.
First example
<Select onValueChange={setLocale}><Select.Label>Langue</Select.Label><Select.Trigger><Select.Value placeholder="Choisir une langue" /><Select.Indicator /></Select.Trigger><Select.Overlay /><Select.Content><Select.Item value="fr">Français</Select.Item><Select.Item value="en">English</Select.Item></Select.Content><Select.Description>Celle de l'interface.</Select.Description></Select>
Anatomy
<Select>
<Select.Label>…</Select.Label>
<Select.Trigger>
<Select.Value />
<Select.Indicator />
</Select.Trigger>
<Select.Overlay />
<Select.Content>
<Select.GroupLabel>…</Select.GroupLabel>
<Select.Item value="…">
<Select.ItemLabel>…</Select.ItemLabel>
<Select.ItemDescription>…</Select.ItemDescription>
<Select.ItemIndicator />
</Select.Item>
</Select.Content>
<Select.Description>…</Select.Description>
<Select.Error>…</Select.Error>
</Select>
Select— the column, and the state and resolved style behind it.Select.Label— what the field is for, above the trigger.Select.Trigger— the control. The field the user sees, and the node arefreaches.Select.Value— what is chosen, or the placeholder until something is.Select.Indicator— the chevron, turning on a spring with the list.Select.Overlay— the backdrop. Optional, and what closes the list on an outside press.Select.Content— the list, positioned against the trigger and rendered in a portal.Select.GroupLabel— a heading over a run of rows, inside the panel.Select.Item— one row.Select.ItemLabel/Select.ItemDescription— its two lines.Select.ItemIndicator— the check on the chosen row.Select.Description/Select.Error— the help line under the field.
The root is the column, a View stacking the label, the trigger and the help line
with one gap — the TextField's shape and the Autocomplete's, token for token, so a
text field and a select on the same form read as one control. ref, style and R14's
style props on the root dress that column; the control itself is Select.Trigger, which
keeps its own ref and its own a11y props.
Select.Overlay and Select.Content render into the nearest PortalHost rather than
where they are written, so they add nothing to the column. Their place in the JSX says
when they exist, not where they appear.
Select.Label is the field's label, not the panel's. The heading over a run of rows
is Select.GroupLabel — the two were one name until the field grew a label of its own.
Usage
Basic
<Select onValueChange={setLocale}>
<Select.Label>Langue</Select.Label>
<Select.Trigger>
<Select.Value placeholder="Choisir une langue" />
<Select.Indicator />
</Select.Trigger>
<Select.Overlay />
<Select.Content>
<Select.Item value="fr">Français</Select.Item>
<Select.Item value="en">English</Select.Item>
</Select.Content>
<Select.Description>Celle de l'interface.</Select.Description>
</Select>
A stringifiable child becomes the row's label (R3), and that same string is what the trigger shows once the row is chosen — the label is written once.
Label, hint and error
<Select isInvalid={!locale} onValueChange={setLocale}>
<Select.Label>Langue</Select.Label>
<Select.Trigger>
<Select.Value placeholder="Choisir une langue" />
<Select.Indicator />
</Select.Trigger>
<Select.Overlay />
<Select.Content>…</Select.Content>
{locale ? (
<Select.Description>Celle de l'interface.</Select.Description>
) : (
<Select.Error>Choisissez une langue.</Select.Error>
)}
</Select>
isInvalid paints the trigger's border, turns the label and the description danger, and
mounts nothing: Select.Error renders exactly what it is given, so the condition stays
where you can read it. The label and the description carry the ids the trigger points at
with aria-labelledby / aria-describedby.
Composed rows
<Select.Item value="fr" label="Français">
<View>
<Select.ItemLabel>Français</Select.ItemLabel>
<Select.ItemDescription>La langue par défaut</Select.ItemDescription>
</View>
<Select.ItemIndicator />
</Select.Item>
label is what the trigger shows. It is needed as soon as the children are not one
string: an avatar and two lines of text have no single label to infer.
The labels are read off the elements, before any row mounts. That matters because the
list lives in a portal that only exists while it is open — without it, a select with a
defaultValue would show its placeholder until the user had opened it once. A row wrapped
in a component of your own is not reachable that way, and children on Select.Value is
the answer for that case.
Controlled
const [value, setValue] = useState('fr')
<Select value={value} onValueChange={setValue}>…</Select>
Open state is controllable the same way, with isOpen / onOpenChange. Which half is
controlled is decided on the first render and then held — a component that changes hands
mid-life produces a bug nobody can read from the call site.
Placement
<Select.Content placement="top" align="start" width="content-fit" offset={12} />
width defaults to 'trigger', because a list wider than the control it drops out of
reads as a different surface. 'content-fit' hugs the longest label instead, bounded by
the screen insets.
avoidCollisions is on by default: the list flips to the other side when the one it was
asked for does not fit. It flips only when the other side has strictly more room, so
a list two points short stays where it was put rather than jumping.
No backdrop
<Select>
<Select.Trigger>…</Select.Trigger>
{/* no Select.Overlay */}
<Select.Content>…</Select.Content>
</Select>
Omit Select.Overlay and only a choice closes the list. That is the answer for a select
inside a sheet that already dims its own background.
Style as props
<Select.Trigger paddingHorizontal={20} borderRadius={8} />
<Select.Content backgroundColor="#111" padding={8} />
<Select.ItemLabel fontSize={18} />
Full RN names, full RN values (R14). Every node takes them — the trigger, the panel, the overlay, each row and each of its texts.
Everything else goes through style
A gradient panel, a tinted shadow, a transform — style is the last word and wins over
everything above it.
Root props
SelectProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
| variant | SelectVariant | undefined | No | — |
| size | SelectSize | undefined | No | — |
| radius | RadiusKey | undefined | No | — |
| color | string | undefined | No | The tint (R7) — a raw value, never a token. |
| value | string | undefined | No | Controlled selection. Leave unset and the root owns it. |
| defaultValue | string | undefined | No | — |
| onValueChange | ((value: string) => void) | undefined | No | — |
| isOpen | boolean | undefined | No | Controlled open state. Leave unset and the root owns it. |
| defaultOpen | boolean | undefined | No | — |
| onOpenChange | ((isOpen: boolean) => void) | undefined | No | — |
| isDisabled | boolean | undefined | No | — |
| isInvalid | boolean | undefined | No | — |
| asChild | boolean | undefined | No | — |
Slots
SelectContentProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
| placement | SelectPlacement | undefined | No | — |
| align | Align | undefined | No | — |
| width | AnchoredWidth | undefined | No | — |
| minWidth | number | undefined | No | A floor under `width`, in points. A `trigger`-width panel that would come out narrower than this takes this instead — still clamped to the screen. `content-fit` measures between it and `maxWidth`. |
| offset | number | undefined | No | Distance from the trigger, in points. |
| alignOffset | number | undefined | No | Shift along the alignment axis, in points. |
| avoidCollisions | boolean | undefined | No | Flip to the other side when the chosen one does not fit. |
| insets | Insets | undefined | No | — |
SelectDescriptionProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
SelectErrorProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
SelectGroupLabelProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
SelectIndicatorProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| as | ComponentType<IconComponentProps> | undefined | No | The glyph. Defaults to the chevron this component ships. |
| size | number | undefined | No | — |
| color | string | undefined | No | A raw value (R7), never a token. Overrides what the trigger asked for. |
SelectItemDescriptionProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
SelectItemIndicatorProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| as | ComponentType<IconComponentProps> | undefined | No | The glyph. Defaults to the check this component ships. |
| size | number | undefined | No | — |
| color | string | undefined | No | A raw value (R7), never a token. |
SelectItemLabelProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
SelectItemProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| value | string | Yes | — |
| label | string | undefined | No | The label the trigger shows once this item is chosen. |
| isDisabled | boolean | undefined | No | — |
| children | ReactNode | ((state: SelectItemRenderState) => ReactNode) | No | — |
| asChild | boolean | undefined | No | — |
SelectLabelProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
SelectOverlayProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
| isDismissable | boolean | undefined | No | Pressing the backdrop closes the list. Set false and only a choice closes it. |
SelectTriggerProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
| asChild | boolean | undefined | No | — |
SelectValueProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| placeholder | string | undefined | No | Shown until something is selected, in the placeholder colour. |
| children | ReactNode | No | The selected value's label, when the caller renders items lazily and the root has never seen them. Unset, the value falls back to the label of the matching item. |
Variants, sizes and colour
Accessibility
The trigger is a button carrying expanded, so a screen reader announces whether the
list is open. Rows are menuitem and carry selected. Select.GroupLabel is a
header, so the group it opens is announced with it. The trigger points at
Select.Label and Select.Description with aria-labelledby / aria-describedby, so a
screen reader announces what the field is for rather than reading the placeholder and
hoping. The overlay announces nothing at all — it is the
absence of the list, and "button" spoken over the whole screen is worse than silence.
Migration from legacy
The legacy component is Select, with SelectItem.
| Legacy | v1 |
|---|---|
<SelectItem value label /> | <Select.Item value label> with its own slots |
label="…" on the root | <Select.Label> |
description="…" | <Select.Description> |
errorMessage="…" | <Select.Error> |
placeholder="…" | placeholder on <Select.Value> |
variant="colored" | variant="primary" |
variant="light" | variant="secondary" |
variant="bordered" | variant="tertiary" |
themeColor="primary" | color={theme.colors.accent} |
selectionMode="multiple" | not in v1 — one value, one control |
customAppearance={{ … }} | style on the slot that key named |
selectionMode is gone. A select that returns several values is a different control
with a different affordance, and calling both by one name is what made the legacy props
list as long as it is. It comes back as its own component or not at all.
Implementation notes
Motion
Three animations, the reference implementation's values throughout.
The chevron turns 0 → −180° on a spring: damping 140, stiffness 1000, mass 4. Heavily damped against a very high stiffness, so it arrives in about a fifth of a second and does not overshoot — an oscillating chevron reads as a bug rather than as motion. It is a worklet, so it keeps turning while JavaScript mounts the rows.
The panel grows out of the trigger: 200 ms in from scale: 0.95 and eight points
offset towards the trigger, so a list below enters upwards and one above enters
downwards. Exit mirrors it at 150 ms — closing is an acknowledgement, not an arrival, and
a dismissal as long as the opening feels like the control is arguing.
The overlay fades on the same two durations.
The measuring pass. The panel mounts invisibly for one frame to learn how tall it
wants to be, then places itself and plays its entrance. Without it avoidCollisions has
nothing to compare, and a list that does not fit below would open downwards off the
screen.
The portal
Select.Overlay and Select.Content need a PortalHost. XAUIProvider mounts one, so
there is nothing to do. An app that needs the host elsewhere — under a gesture root, or
inside its own navigation container — sets hasPortalHost={false} on the provider and
mounts a PortalHost itself.
Without a host anywhere, Portal renders nothing: the select opens onto an empty screen
with no error. That silence is why the provider mounts one by default.