Persona

Select

Inputs · 26 props · Updated Sep 22, 2026

Select is a button that states the current value and opens a menu of options to change it. Radix’s select primitive supplies the menu, the keyboard and the ARIA roles; PDS supplies the field, the option rows, and the wiring to FormField — already done for you in SelectField, which is the component to render.

  • Container the field. Closed, it states the current answer, or the placeholder when there is not one yet.
  • Chevron icon the promise that clicking opens something. It is drawn by the component, always.
  • Menu (when open) the surface the options sit on, anchored to the field and matching its width.
  • Option (when open) one choice. The selected one keeps a check when the menu is open.

Closed, a select shows its value and not its control, so a form built from them reads back as a set of answers rather than a set of questions. That is what the component is for: options that are known, short enough to scan, and less interesting than the answer.

The cost is that every option is hidden behind a click, which is the right trade only for a reader who already knows roughly what they want. Where there are few enough options to leave on screen, Radio Buttons and Checkboxes spend space to save that click. A Combobox takes over once the reader would rather filter than scan, or needs more than one answer, and a List Box once the options have to stay visible. A Dropdown Button looks similar and is not related: its menu items are actions, not values.

SelectField reconciles the trigger’s id with the label’s

Section titled “SelectField reconciles the trigger’s id with the label’s”

SelectField takes FormField’s props and Select’s props in one object, and the Option children as children. What it fixes is narrower than convenience: Select’s own id prop wins over the id FormField generates, so a Select given an id inside a hand-composed FormField renders a label whose for attribute points at an id nothing in the field carries. SelectField derives one id, gives it to the trigger, and sets the label’s htmlFor to match, whether you passed an id or not.

A hand-composed FormField is still the way to put a select beside a second control in one field, since the input slot then holds something no composed field wraps.

The menu takes its width from the trigger, through a Radix variable, so there is no prop that makes the menu wider than the field it belongs to. width and minWidth set the field, and the menu follows.

Lift the 225-pixel menu cap only for a list that nearly fits

Section titled “Lift the 225-pixel menu cap only for a list that nearly fits”

The menu scrolls past 225 pixels, and maxHeight replaces that default. A menu is easier to read than to scroll, so lift the cap where the list is only a little too long, and leave it where lifting it would put a menu taller than the window on screen.

The menu renders children in the order you pass them and never sorts. Order is the only affordance a closed select has, so put the answer most readers want first rather than alphabetizing by reflex: alphabetical order helps a reader who already knows the word they are looking for, and nobody else.

A portalled menu escapes a clipping ancestor, and a dialog’s dismiss scope

Section titled “A portalled menu escapes a clipping ancestor, and a dialog’s dismiss scope”

portal renders the menu at the end of the document, which gets it out of any ancestor with overflow: hidden. Inside a Modal, pass portalContainer as well, pointing at an element inside the dialog. A menu portalled all the way out leaves the dialog’s dismiss scope, so the first click on an option reads as a click outside and closes the dialog under it.

onValueChange is Radix’s, and onChange is a shim that hands you { target: { value } } for react-hook-form. Both run on every change, so use one.

value={null} and value="" both count as empty and show the placeholder again. Radix on its own treats null as uncontrolled; PDS converts it, which is what makes “clear this field” a value you can pass rather than a state you have to model.

Radix owns the roles, and FormField adds the label

Section titled “Radix owns the roles, and FormField adds the label”

The trigger, the menu and the options get their roles and their expanded and selected state from Radix. None of it is a prop, and setting it by hand through triggerProps fights the primitive rather than helping it.

What FormField adds is the label and the description wiring: the trigger picks up aria-describedby from the helper text, and aria-invalid when the field has an error. Select reads that error state out of context, so errorText on a SelectField borders the trigger red and marks it invalid with nothing else passed — unlike Text Area, which needs hasError of its own. Where there is no visible label at all, aria-label is the fallback.

aria-errormessage points at the helper text’s id rather than the error text’s, so a field whose only message is an error has nothing to announce. Until that is fixed upstream, put the substance of the error in helper text as well. See FormField.

Nobody reads an option list; they scan it. An option carries what distinguishes it from the options above and below it, and nothing more, because every extra word is one the reader has to discard in every row. A second line goes in subtitle on Option, which renders it smaller and in secondary text so it can be skipped.

Put an instruction in the placeholder, and the noun in the label

Section titled “Put an instruction in the placeholder, and the noun in the label”

“Select country” is an instruction, and that is right here even though Writing asks placeholders elsewhere to be examples. Nothing can be typed into a select, so its placeholder is the absence of a value rather than a hint about the shape of one. The label still reads “Country”; the field needs both.

SelectField is every prop below plus the twelve FormField carries. Its table comes first.

NameType
label

ReactNode

Renders <Label> above input

inputId

string

ID of the input element. Providing this will automatically link the label to the input.

helperText

ReactNode

Renders <HelperText> below input

errorText

ReactNode

Renders <ErrorText> below input and helper text.

warningText

ReactNode

Renders <WarningText> below input and helper text.

labelProps

FormLabelProps

helperProps

(HelperTextProps & RefAttributes<HTMLDivElement>)

warningProps

(Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, “ref”> & RefAttributes<HTMLDivElement>)

errorProps

(Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, “ref”> & RefAttributes<HTMLDivElement>)

configField

boolean

Whether to render the form field as a config field. This will render the helper text as a tooltip.

configFieldInputWidth

string

Override the width of the input area when configField is true. Accepts any valid CSS width value (e.g. ‘33%’, ‘66.67%’). Defaults to ‘50%‘.

rightActions

ReactNode

Renders actions to the right of the label.

aria-label

string

placeholder

string

id

string

id will be applied to trigger

className

string

Class name will be applied to trigger

maxHeight

number

Max height of options menu

minWidth

number

Min width of the select

valueContainerWidth

auto, 100%

Width of the selected value container

portal

boolean

Render dropdown menu in a portal.

portalContainer

HTMLElement, null

Element the portalled menu renders into. Defaults to document.body. Pass a container inside the current dialog so the menu escapes clipping ancestors without leaving the dialog’s dismiss scope, which would close it on click. Requires portal.

triggerProps

SelectTriggerProps

Props to apply to Radix Select.Trigger within

contentProps

SelectContentProps

Props to apply to Radix Select.Content within

onChange

((event: { target: { value: string; }; }) => void)

Callback meant for use with react-hook-form’s useForm (or Persona’s useFormSecurely)

size

InputSizes

value

string, null

width

string

header

ReactNode

footer

ReactNode

zIndex

ZIndex

z-index of content. Please use sparingly.

hasError

boolean

hasWarning

boolean

hovered

boolean

Visual state override. Forces the :hover styling on permanently, whatever the real state is. For specimens and visual tests, not application code.

focused

boolean

Visual state override. Forces the :focus styling on permanently, whatever the real state is. For specimens and visual tests, not application code.

active

boolean

Visual state override. Forces the :focus-within styling on permanently, whatever the real state is. For specimens and visual tests, not application code.

disabled

boolean

checked

boolean

Visual state override. Forces the :checked styling on permanently, whatever the real state is. For specimens and visual tests, not application code.

mixed

boolean

Visual state override. Forces the :indeterminate styling on permanently, whatever the real state is. For specimens and visual tests, not application code.

Also accepts every prop of Omit<SelectPrimitive.SelectProps, ‘value’>.

Select’s own props, which SelectField also accepts:

NameType
aria-label

string

placeholder

string

id

string

id will be applied to trigger

className

string

Class name will be applied to trigger

maxHeight

number

Max height of options menu

minWidth

number

Min width of the select

valueContainerWidth

auto, 100%

Width of the selected value container

portal

boolean

Render dropdown menu in a portal.

portalContainer

HTMLElement, null

Element the portalled menu renders into. Defaults to document.body. Pass a container inside the current dialog so the menu escapes clipping ancestors without leaving the dialog’s dismiss scope, which would close it on click. Requires portal.

triggerProps

SelectTriggerProps

Props to apply to Radix Select.Trigger within

contentProps

SelectContentProps

Props to apply to Radix Select.Content within

onChange

((event: { target: { value: string; }; }) => void)

Callback meant for use with react-hook-form’s useForm (or Persona’s useFormSecurely)

size

InputSizes

value

string, null

width

string

header

ReactNode

footer

ReactNode

zIndex

ZIndex

z-index of content. Please use sparingly.

hasError

boolean

hasWarning

boolean

hovered

boolean

Visual state override. Forces the :hover styling on permanently, whatever the real state is. For specimens and visual tests, not application code.

focused

boolean

Visual state override. Forces the :focus styling on permanently, whatever the real state is. For specimens and visual tests, not application code.

active

boolean

Visual state override. Forces the :focus-within styling on permanently, whatever the real state is. For specimens and visual tests, not application code.

disabled

boolean

checked

boolean

Visual state override. Forces the :checked styling on permanently, whatever the real state is. For specimens and visual tests, not application code.

mixed

boolean

Visual state override. Forces the :indeterminate styling on permanently, whatever the real state is. For specimens and visual tests, not application code.

Also accepts every prop of Omit<SelectPrimitive.SelectProps, ‘value’>.

Was this page helpful?