Select and MenuItem
Pick one value, or several, from a list of options.
Select shows a trigger and a menu of MenuItem options. It builds on React Aria's Select,
Popover and ListBox, and sits in the same input frame as TextField, so the label, outline and
underline behave the same. For a labeled field with helper text, use <TextField select>.
Both components are client components and read theme defaultProps.
import Select from '@hone-ui/react/Select';
import MenuItem from '@hone-ui/react/MenuItem';Examples
<FormControl sx={{ minWidth: 200 }}>
<InputLabel id="size-label">Size</InputLabel>
<Select labelId="size-label" label="Size" defaultValue="m">
<MenuItem value="s">Small</MenuItem>
<MenuItem value="m">Medium</MenuItem>
<MenuItem value="l">Large</MenuItem>
</Select>
</FormControl>InputLabel htmlFor with Select id links the label too. Use it when you cannot share a label id:
<FormControl sx={{ minWidth: 200 }}>
<InputLabel htmlFor="size-select">Size</InputLabel>
<Select id="size-select" label="Size" defaultValue="m">
<MenuItem value="s">Small</MenuItem>
<MenuItem value="m">Medium</MenuItem>
</Select>
</FormControl>A controlled select reads value from your state:
<FormControl sx={{ minWidth: 200 }}>
<InputLabel id="age-label">Age</InputLabel>
<Select
labelId="age-label"
label="Age"
value={age}
onChange={(event) => setAge(Number(event.target.value))}
>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
</Select>
</FormControl>Multiple selection takes an array and keeps the menu open.
<Select
multiple
value={names}
onChange={(event) => setNames(event.target.value)}
aria-label="Names"
>
{all.map((name) => (
<MenuItem key={name} value={name}>
{name}
</MenuItem>
))}
</Select>startAdornment and endAdornment render inside the frame. The start one sits before the value
and shrinks the label. The end one sits between the value and the arrow.
<Stack spacing={2} sx={{ maxWidth: 260 }}>
<TextField
select
label="Weight"
defaultValue="10"
slotProps={{ input: { startAdornment: <span>~</span>, endAdornment: <span>kg</span> } }}
>
<MenuItem value="10">Ten</MenuItem>
<MenuItem value="20">Twenty</MenuItem>
</TextField>
<Select
aria-label="Units"
defaultValue="10"
startAdornment={<span>#</span>}
endAdornment={<span>units</span>}
>
<MenuItem value="10">Ten</MenuItem>
<MenuItem value="20">Twenty</MenuItem>
</Select>
</Stack>Native select
native renders a real select with option children in the same frame, with the same arrow.
The browser draws the options, so long lists and touch screens get the platform picker, and a form
works without JavaScript. onChange gets the real ChangeEvent<HTMLSelectElement>.
<Stack spacing={2} sx={{ maxWidth: 260 }}>
<TextField select label="Size" defaultValue="m" SelectProps={{ native: true }}>
<option value="s">Small</option>
<option value="m">Medium</option>
<option value="l">Large</option>
</TextField>
<Select native aria-label="Fruit" defaultValue="apple">
<option value="apple">Apple</option>
<option value="pear">Pear</option>
</Select>
</Stack>It takes value, defaultValue, multiple, name, disabled, required, readOnly, error,
adornments, id, labelId and the aria-* props. A label with htmlFor set to the Select id
names it. inputRef receives the select. The menu props (open, onOpen, onClose,
renderValue, displayEmpty, autoWidth, MenuProps) do nothing. Add <option value="" /> for a
blank choice, because a select with an unmatched value shows its first option.
readOnly keeps the trigger focusable but never opens the menu or changes the value. A form still
posts the value.
<Select readOnly aria-label="Plan" defaultValue="pro" sx={{ minWidth: 200 }}>
<MenuItem value="free">Free</MenuItem>
<MenuItem value="pro">Pro</MenuItem>
</Select>An empty Select is only as wide as its content. Set a minWidth through sx or style, or use
fullWidth.
The change event
React Aria reports a selection with no DOM event, so Select does not fire a real
change event. onChange gets a SelectChangeEvent object that Select builds.
typeis'change'.targetandcurrentTargetare the same{ value, name }object. It is not an element.nativeEventis the DOM event running at that moment, or a newEvent('change')when none is.preventDefault,stopPropagation,isDefaultPrevented,isPropagationStoppedandpersistexist and only record state.
value keeps the type of the MenuItem value. A number stays a number and an object stays the
same object. Values match, with objects by identity and everything else by string form.
Select props
| Prop | Type | Default | Description |
|---|---|---|---|
children | MenuItem elements | none | The options. They may sit inside fragments. |
value | any, or an array with multiple | none | Makes the Select controlled. |
defaultValue | any | '', or [] with multiple | The starting value when uncontrolled. |
onChange | (event: SelectChangeEvent, child) => void | none | child is the chosen MenuItem element. Not called when the user picks the current value. |
multiple | boolean | false | value becomes an array. |
native | boolean | false | Renders a real select with option children. onChange gets the real change event. |
displayEmpty | boolean | false | Shows the matching option or renderValue for an empty value. |
renderValue | (value) => ReactNode | none | Custom trigger content. Skipped for an empty value unless displayEmpty. |
label | ReactNode | none | Sizes the notch of the outlined frame. Match the InputLabel's text. |
labelId | string | none | The id of the InputLabel. The trigger takes its name from it. |
variant | 'outlined', 'filled', 'standard' | 'outlined' | Falls back to the FormControl. |
size | 'small', 'medium' | 'medium' | The size. |
color | palette color | 'primary' | The focused color. |
name | string | none | Goes to a hidden input and to event.target.name. |
disabled, error, required, fullWidth, hiddenLabel | boolean | false | Fall back to the FormControl. |
autoFocus | boolean | false | Focuses the trigger. |
startAdornment, endAdornment | ReactNode | none | Inside the frame. The start one shrinks the label. The end one sits before the arrow. |
readOnly | boolean | false | Focusable, but the menu never opens and the value cannot change. A form still posts it. |
notched | boolean | none | Opens the outline gap for the label. Unset, it follows focus, a value or a start adornment. |
autoWidth | boolean | false | The menu fits its content instead of at least the trigger width. |
open, defaultOpen, onOpen, onClose | Control the menu. The event is a DOM event or undefined. | ||
onFocus, onBlur | (event) => void | none | React focus events of the trigger. |
IconComponent | ElementType | drop-down arrow | Receives className. |
id | string | none | Goes to the trigger. Use slotProps.root.id for the root. |
aria-label, aria-labelledby, aria-describedby | string | none | Go to the trigger. |
inputRef | Ref | none | A ref to the hidden input. |
slotProps | { root, trigger, input, menu, list } | none | menu and list take className, sx and style. |
MenuProps, SelectDisplayProps, inputProps | object | none | Legacy aliases of slotProps.menu, trigger and input. |
sx, className, style, ref | Standard. They go to the root frame. |
MenuItem props
| Prop | Type | Default | Description |
|---|---|---|---|
value | any | none | Passed to onChange and compared with the Select's value. |
children | ReactNode | none | The option's content. The trigger shows it when chosen. |
disabled | boolean | false | Cannot be chosen. Sets aria-disabled. |
selected | boolean | false | Draws the selected look. Select sets the real selection. |
dense | boolean | false | Less padding and smaller text. |
divider | boolean | false | A line under the row. Material look only. |
textValue | string | text of children | Type-ahead text. Set it for element children. |
sx, className, style, ref | Standard. ref is an HTMLDivElement. |
Notes
onChangereceives a plainSelectChangeEvent, not a DOM event.- MenuItem renders a
divwithrole="option", not anli. Anidyou pass replaces React Aria's generated id. - Not supported on Select are
input, menu positioning and transition props such asMenuProps.anchorOrigin, andslots. - Not supported on MenuItem are
autoFocus,component,disableGutters,focusVisibleClassNameandhref. - MenuItem works only inside a Select or a React Aria ListBox.
Accessibility
The trigger is a button with aria-haspopup="listbox" and aria-expanded. Give every Select a
name through labelId, aria-labelledby or aria-label. The menu is a listbox of option
elements with aria-selected. Enter, Space or the arrow keys open it. Arrow keys, Home, End and
type-ahead move through it, Enter chooses, and Escape closes it and returns focus to the trigger.
The form value comes from a hidden input that carries name, with arrays joined by commas.