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.
Anatomy
Section titled “Anatomy”-
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.
When to use
Section titled “When to use”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
Section titled “The menu”Widen the field to widen the menu
Section titled “Widen the field to widen the menu”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.
Order the options yourself
Section titled “Order the options yourself”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.
Behavior
Section titled “Behavior”Use one of the two change callbacks
Section titled “Use one of the two change callbacks”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.
Pass null to clear the field
Section titled “Pass null to clear the field”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.
Accessibility
Section titled “Accessibility”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.
Content
Section titled “Content”Write options to be scanned, not read
Section titled “Write options to be scanned, not read”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.
| Name | Type |
|---|---|
label |
Renders |
inputId |
ID of the input element. Providing this will automatically link the label to the input. |
helperText |
Renders |
errorText |
Renders |
warningText |
Renders |
labelProps |
|
helperProps |
|
warningProps |
|
errorProps |
|
configField |
Whether to render the form field as a config field. This will render the helper text as a tooltip. |
configFieldInputWidth |
Override the width of the input area when |
rightActions |
Renders actions to the right of the label. |
aria-label |
|
placeholder |
|
id |
id will be applied to trigger |
className |
Class name will be applied to trigger |
maxHeight |
Max height of options menu |
minWidth |
Min width of the select |
valueContainerWidth |
Width of the selected value container |
portal |
Render dropdown menu in a portal. |
portalContainer |
Element the portalled menu renders into. Defaults to |
triggerProps |
Props to apply to Radix Select.Trigger within |
contentProps |
Props to apply to Radix Select.Content within |
onChange |
Callback meant for use with |
size |
|
value |
|
width |
|
header |
|
footer |
|
zIndex |
z-index of content. Please use sparingly. |
hasError |
|
hasWarning |
|
hovered |
Visual state override. Forces the |
focused |
Visual state override. Forces the |
active |
Visual state override. Forces the |
disabled |
|
checked |
Visual state override. Forces the |
mixed |
Visual state override. Forces the |
Omit<SelectPrimitive.SelectProps, ‘value’>.Select’s own props, which SelectField also accepts:
| Name | Type |
|---|---|
aria-label |
|
placeholder |
|
id |
id will be applied to trigger |
className |
Class name will be applied to trigger |
maxHeight |
Max height of options menu |
minWidth |
Min width of the select |
valueContainerWidth |
Width of the selected value container |
portal |
Render dropdown menu in a portal. |
portalContainer |
Element the portalled menu renders into. Defaults to |
triggerProps |
Props to apply to Radix Select.Trigger within |
contentProps |
Props to apply to Radix Select.Content within |
onChange |
Callback meant for use with |
size |
|
value |
|
width |
|
header |
|
footer |
|
zIndex |
z-index of content. Please use sparingly. |
hasError |
|
hasWarning |
|
hovered |
Visual state override. Forces the |
focused |
Visual state override. Forces the |
active |
Visual state override. Forces the |
disabled |
|
checked |
Visual state override. Forces the |
mixed |
Visual state override. Forces the |
Omit<SelectPrimitive.SelectProps, ‘value’>.Was this page helpful?