The field that picks one value from a short, fixed list: a labelled box showing the chosen option, or a placeholder, that opens a list of options under it, with the field's help and error round it.
When to use
Use Select for one choice from a short, fixed list, such as a transport mode or an order.
| Use instead | When |
|---|---|
| SearchField | The person finds a line or a stop among hundreds, narrowing the list as they type |
| RadioGroup | Two to six choices that the person should read in full before choosing |
| SegmentedControl | The choice is how one view shows its content, one of two to five |
| Menu | The list holds actions, not a value to keep |
Kongens Nytorv
Metro M1, M2 and M3.
Props
| Name | Type | Default | Description |
|---|---|---|---|
items* | readonly SelectGroup[] | readonly SelectItem[] | — | Required. The options, in order: each with a value that identifies it and the label shown in its row and in the box once chosen, and disabled for one that can't be chosen; or groups, each with a label and its own items. |
placeholder | string | — | Words shown in the box while no option is chosen, such as "Choose a transport mode". They never replace the label. |
value | string | null | — | The chosen option's value, or null for none, when the caller controls it. |
defaultValue | string | null | — | The option chosen at first, when Select keeps the value itself. Left out, nothing is chosen. |
onChange | (event: Event, value: string | null, details: Select.Root.ChangeEventDetails) => void | — | Called when the chosen option changes: by a press or Enter on a row (details.reason 'item-press'); or, with 'none', by letters typed on the closed box, by the browser's autofill, or when the chosen option leaves items, which puts back the value given at first, while that option is still in items, or else null. event is details.event. |
open | boolean | — | Whether the list of options is shown, when the caller controls it. |
defaultOpen | boolean | false | Whether the list is shown at first, when open is not set. |
onOpen | (event: Event, details: Select.Root.OpenChangeEventDetails) => void | — | Called when the list asks to open: by a press of the box, Enter or Space (details.reason 'trigger-press'), or an arrow key ('list-navigation'). |
onClose | (event: Event, reason: Select.Root.ChangeEventReason, details: Select.Root.OpenChangeEventDetails) => void | — | Called when the list asks to close: reason is 'item-press', 'escape-key', 'outside-press', 'trigger-press', 'focus-out', 'cancel-open' or another of Base UI's reasons; details.cancel() keeps it open. |
required | boolean | false | Whether an option must be chosen before the form is submitted. Use it or optional, never both. |
readOnly | boolean | false | Whether the chosen option is shown but can't be changed. The list still opens, so the person can see the options. |
label* | ReactNode | — | Required. The field's name, in sentence case, such as "Stop name": shown above the control, and the control's accessible name. |
hiddenLabel | boolean | false | Whether the label is heard but not seen. Use it only where the screen around the field already says what it is for. |
optional | boolean | false | Whether the person may leave the field empty. Adds "(optional)" after the label's words, seen and heard as part of the control's name. Set it on the few fields a form doesn't ask for; Field draws no mark for a required one. |
optionalLabel | string | The locale's | Overrides "(optional)", the mark after the label's words with optional. |
description | ReactNode | — | A line of help under the label, such as "As written on the stop sign", read after the label by a screen reader. Left out, no description is rendered. |
error | ReactNode | — | What is wrong with the value and how to put it right, such as "Enter a stop name". Given, it marks the field invalid and is shown under the control; left out, the field shows Base UI's own message when the control fails its checks. |
ref | Ref<HTMLDivElement> | — | Reaches the root, the Field.Root <div>. |
slotProps | { label?: FieldLabelProps | undefined; description?: FieldDescriptionProps | undefined; error?: FieldErrorProps | undefined; trigger?: SelectTriggerProps | undefined; value?: SelectValueProps | undefined; icon?: SelectIconProps | undefined; portal?: SelectPortalProps | undefined; positioner?: Omit<SelectPositionerProps, "alignItemWithTrigger"> | undefined; popup?: SelectPopupProps | undefined; list?: SelectListProps | undefined; group?: SelectGroupProps | undefined; groupLabel?: SelectGroupLabelProps | undefined; separator?: SelectSeparatorProps | undefined; item?: SelectItemProps | undefined; itemText?: SelectItemTextProps | undefined; itemIndicator?: SelectItemIndicatorProps | undefined; } | — | Props for each part: label, description, error, trigger, value, icon, portal, positioner, popup, list, group, groupLabel, separator, item, itemText, itemIndicator. |
items (required)readonly SelectGroup[] | readonly SelectItem[], default —- Required. The options, in order: each with a
valuethat identifies it and thelabelshown in its row and in the box once chosen, anddisabledfor one that can't be chosen; or groups, each with alabeland its ownitems. placeholderstring, default —- Words shown in the box while no option is chosen, such as "Choose a transport mode". They never replace the label.
valuestring | null, default —- The chosen option's
value, ornullfor none, when the caller controls it. defaultValuestring | null, default —- The option chosen at first, when Select keeps the value itself. Left out, nothing is chosen.
onChange(event: Event, value: string | null, details: Select.Root.ChangeEventDetails) => void, default —- Called when the chosen option changes: by a press or Enter on a row (
details.reason'item-press'); or, with'none', by letters typed on the closed box, by the browser's autofill, or when the chosen option leavesitems, which puts back the value given at first, while that option is still initems, or elsenull.eventisdetails.event. openboolean, default —- Whether the list of options is shown, when the caller controls it.
defaultOpenboolean, defaultfalse- Whether the list is shown at first, when
openis not set. onOpen(event: Event, details: Select.Root.OpenChangeEventDetails) => void, default —- Called when the list asks to open: by a press of the box, Enter or Space (
details.reason'trigger-press'), or an arrow key ('list-navigation'). onClose(event: Event, reason: Select.Root.ChangeEventReason, details: Select.Root.OpenChangeEventDetails) => void, default —- Called when the list asks to close:
reasonis'item-press','escape-key','outside-press','trigger-press','focus-out','cancel-open'or another of Base UI's reasons;details.cancel()keeps it open. requiredboolean, defaultfalse- Whether an option must be chosen before the form is submitted. Use it or
optional, never both. readOnlyboolean, defaultfalse- Whether the chosen option is shown but can't be changed. The list still opens, so the person can see the options.
label (required)ReactNode, default —- Required. The field's name, in sentence case, such as "Stop name": shown above the control, and the control's accessible name.
hiddenLabelboolean, defaultfalse- Whether the label is heard but not seen. Use it only where the screen around the field already says what it is for.
optionalboolean, defaultfalse- Whether the person may leave the field empty. Adds "(optional)" after the label's words, seen and heard as part of the control's name. Set it on the few fields a form doesn't ask for; Field draws no mark for a required one.
optionalLabelstring, default The locale's- Overrides "(optional)", the mark after the label's words with
optional. descriptionReactNode, default —- A line of help under the label, such as "As written on the stop sign", read after the label by a screen reader. Left out, no description is rendered.
errorReactNode, default —- What is wrong with the value and how to put it right, such as "Enter a stop name". Given, it marks the field invalid and is shown under the control; left out, the field shows Base UI's own message when the control fails its checks.
refRef<HTMLDivElement>, default —- Reaches the root, the
Field.Root<div>. slotProps{ label?: FieldLabelProps | undefined; description?: FieldDescriptionProps | undefined; error?: FieldErrorProps | undefined; trigger?: SelectTriggerProps | undefined; value?: SelectValueProps | undefined; icon?: SelectIconProps | undefined; portal?: SelectPortalProps | undefined; positioner?: Omit<SelectPositionerProps, "alignItemWithTrigger"> | undefined; popup?: SelectPopupProps | undefined; list?: SelectListProps | undefined; group?: SelectGroupProps | undefined; groupLabel?: SelectGroupLabelProps | undefined; separator?: SelectSeparatorProps | undefined; item?: SelectItemProps | undefined; itemText?: SelectItemTextProps | undefined; itemIndicator?: SelectItemIndicatorProps | undefined; }, default —- Props for each part:
label,description,error,trigger,value,icon,portal,positioner,popup,list,group,groupLabel,separator,item,itemText,itemIndicator.
Use it
import { Select } from '@mylder/perron'Ask your agent
get_reference({ library: "base-ui", component: "Select" })Keyboard and ARIA
From the Base UI reference for Select and the installed SelectRoot, SelectTrigger, SelectPopup, SelectList, SelectItem, SelectGroup, SelectGroupLabel and SelectSeparator, and Field's for the label. "From" says who does it.
| Key | Does | From |
|---|---|---|
Enter, Space | On the closed trigger: open the list (onOpen, 'trigger-press'), highlighting and focusing the chosen option, or the first. On a highlighted row: choose it (onChange, 'item-press'), close the list (onClose, 'item-press') and return focus to the trigger | Base UI |
ArrowDown, ArrowUp | On the closed trigger: open the list (onOpen, 'list-navigation'), highlighting and focusing the chosen option, or, with nothing chosen, the first on ArrowDown and the last on ArrowUp. In the open list: move the highlight and focus to the next or previous row, disabled rows included, stopping at the first and the last | Base UI |
Home, End | In the open list: move the highlight and focus to the first or the last row | Base UI |
| Typing letters | On the closed trigger: choose the next option whose label starts with them (onChange, 'none'), without opening the list. In the open list: move the highlight to it. Disabled options are skipped | Base UI |
Escape | In the open list: close it (onClose, 'escape-key') and return focus to the trigger, keeping the value | Base UI |
Tab, Shift+Tab | On the trigger: move focus out of the field, the trigger being its one tab stop. In the open list: close it (onClose, 'focus-out') and return focus to the trigger, so a second Tab leaves the field | Base UI |
| A press on the label | Focuses the trigger, without opening the list (Field's label, with nativeLabel={false}) | Base UI |
| Element | Role | Accessible name from | States announced |
|---|---|---|---|
label | none: a <div>, or a hidden span | Its text: the caller's label, then "(optional)" with optional | none |
trigger | combobox, from Base UI, on a native <button> | aria-labelledby the label | aria-expanded, aria-haspopup="listbox", aria-required, aria-readonly, aria-invalid, disabled; its value is its text, the chosen option's label or the placeholder |
value, icon | none: <span>s; the icon aria-hidden from Base UI | none | none |
list | listbox, from Base UI | aria-labelledby the label, which Perron sets | aria-readonly while read-only |
group | group, from Base UI | aria-labelledby its groupLabel, from Base UI | none |
groupLabel | none: a <div>, aria-hidden from Base UI | none: heard as the group's name only | none |
separator | presentation, from Base UI | none | none |
item | option, from Base UI | Its text: the item's label | aria-selected, aria-disabled; focused while highlighted |
itemIndicator | none: the SelectedMark's aria-hidden tick | none: the row's aria-selected says it | none |
positioner, popup | none, and presentation, from Base UI | none | none |
Base UI's hidden <input> | none: aria-hidden, tabindex="-1" | none | none |
description, error | none, as Field gives them | none | Read as the trigger's description, through aria-describedby, while mounted |
What axe won't catch, from the contract, each with how this component meets it:
- Everything the eye is told, the ear is told, for every shape each prop accepts: the chevron is
aria-hiddenbeside a trigger thecomboboxrole andaria-expandeddescribe; the selected mark isaria-hiddenon a row whosearia-selectedsays it is chosen; a hidden label still names the trigger and the list; a group label is heard as its group's name; a disabled row isaria-disabled, not only grey; an invalid field isaria-invalidand its error is words, never only the trigger's red edge. - Every landmark and named region has an accessible name: none here; Select is not a landmark, and its list is named by the label.
- Every default label can be overridden:
optionalLabel, Field's, overrides "(optional)", the one owned string Select shows (see Owned strings). - Every shape the type accepts renders, in every variant:
label,descriptionanderrorrender a string, an element and a list alike, as Field's do;itemsrenders a flat list and groups alike; a long Danish compound wraps in a row and a group label and ends in an ellipsis in the box (CR-11). - A hidden copy kept for animation is
aria-hidden, and a collapsed region isinert: none here; the closed list renders nothing, and Base UI makes the positionerinertwhile it closes; Base UI's hidden input isaria-hidden. - Native semantics first: a native
<button>for the trigger, and Base UI'scombobox,listbox,groupandoptionroles; Perron adds only the listbox'saria-labelledby. The label is a<div>rather than a<label>, as Base UI advises for a<button>control, and names the trigger througharia-labelledby. - Focus moves in reading order and returns on close: the trigger is the field's one tab stop; opening moves focus to the chosen row, and choosing, Esc, Tab and a press outside return it to the trigger, through Base UI's popup.
- Every target is at least
size.target-minsquare: a row, at leastsize.target-mintall and as wide as the list; the trigger,size.inputtall and as wide as the field. - Loops stop when the system asks for less motion: none here; Select has no loop and no transition (CR-12).
Owned strings
| Key | English | Override | Seen or heard where |
|---|---|---|---|
optional | "(optional)" | optionalLabel | optional, Field's, seen after the label's words with optional, and heard in the trigger's name, as "Transport mode (optional)" |
optional is the key packages/perron/src/locale/en.ts already has, "A field's label: after the words of a field the person may leave empty", which Field reads and Select passes optionalLabel to. Select adds no key.
Every other word is the caller's, through label, description, error, placeholder and items, or Base UI's message for a failing check, which is the caller's validate result or the browser's own, Field's known exception: a required select with nothing chosen fails the hidden input's native check, whose message the browser writes in its own interface language, unless the caller gives error or slotProps.error.match with its own words.
Do and don't
For a caller using the component, with the reason. Each row is a rule: a must row's Don't is never right, a should row's needs a reason, and Checked by says what catches the Don't in a caller's code.
| Rule | Level | Do | Don't | Checked by |
|---|---|---|---|---|
| DD-1 | should | Use Select for one choice from a short, fixed list, such as a transport mode or an order | Use it to find a line or a stop among hundreds: use SearchField, which narrows the list as the person types | review |
| DD-2 | must | Give a label that names the value, "Transport mode", and hide it with hiddenLabel only where the screen already says it | Let the placeholder stand in for the label: it is gone once an option is chosen, and drawn in color.text.tertiary, below text contrast | review |
| DD-3 | must | Write the placeholder as an instruction, "Choose a transport mode" | Put an option's words in the placeholder: the person reads it as a value already chosen | review |
| DD-4 | should | Give short labels that the person can tell apart by their first letters, so typing on the closed box finds them | Start every option with the same word ("Line M1", "Line M2"): typing finds nothing, and the box's ellipsis cuts the words that differ | review |
| DD-5 | must | Group options with SelectGroup when the list mixes kinds, such as metro and S-tog lines, and write each group's label in sentence case | Write a group label in capitals: its style sets the capitals, and the ear hears the words as written | review |
| DD-6 | must | Set required or optional, whichever the form needs | Set both: the field is marked optional while the form refuses it empty | review |
| DD-7 | must | Give error whenever the field is invalid | Set invalid with no message: the trigger's red edge alone tells only the eye, and only by its colour | review |
| DD-8 | should | Leave an option that can't be chosen out of items, or give it disabled when the person should see it is there | Disable the whole field to say one option isn't available | review |
| DD-9 | should | Keep the field in a PerronProvider with the screen's locale, so "(optional)" speaks it | Translate it with optionalLabel on every instance: that is for one instance's own words | review |