Hone UI
Components

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.

~
kg
#
units
<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.

  • type is 'change'.
  • target and currentTarget are the same { value, name } object. It is not an element.
  • nativeEvent is the DOM event running at that moment, or a new Event('change') when none is.
  • preventDefault, stopPropagation, isDefaultPrevented, isPropagationStopped and persist exist 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

PropTypeDefaultDescription
childrenMenuItem elementsnoneThe options. They may sit inside fragments.
valueany, or an array with multiplenoneMakes the Select controlled.
defaultValueany'', or [] with multipleThe starting value when uncontrolled.
onChange(event: SelectChangeEvent, child) => voidnonechild is the chosen MenuItem element. Not called when the user picks the current value.
multiplebooleanfalsevalue becomes an array.
nativebooleanfalseRenders a real select with option children. onChange gets the real change event.
displayEmptybooleanfalseShows the matching option or renderValue for an empty value.
renderValue(value) => ReactNodenoneCustom trigger content. Skipped for an empty value unless displayEmpty.
labelReactNodenoneSizes the notch of the outlined frame. Match the InputLabel's text.
labelIdstringnoneThe 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.
colorpalette color'primary'The focused color.
namestringnoneGoes to a hidden input and to event.target.name.
disabled, error, required, fullWidth, hiddenLabelbooleanfalseFall back to the FormControl.
autoFocusbooleanfalseFocuses the trigger.
startAdornment, endAdornmentReactNodenoneInside the frame. The start one shrinks the label. The end one sits before the arrow.
readOnlybooleanfalseFocusable, but the menu never opens and the value cannot change. A form still posts it.
notchedbooleannoneOpens the outline gap for the label. Unset, it follows focus, a value or a start adornment.
autoWidthbooleanfalseThe menu fits its content instead of at least the trigger width.
open, defaultOpen, onOpen, onCloseControl the menu. The event is a DOM event or undefined.
onFocus, onBlur(event) => voidnoneReact focus events of the trigger.
IconComponentElementTypedrop-down arrowReceives className.
idstringnoneGoes to the trigger. Use slotProps.root.id for the root.
aria-label, aria-labelledby, aria-describedbystringnoneGo to the trigger.
inputRefRefnoneA ref to the hidden input.
slotProps{ root, trigger, input, menu, list }nonemenu and list take className, sx and style.
MenuProps, SelectDisplayProps, inputPropsobjectnoneLegacy aliases of slotProps.menu, trigger and input.
sx, className, style, refStandard. They go to the root frame.
PropTypeDefaultDescription
valueanynonePassed to onChange and compared with the Select's value.
childrenReactNodenoneThe option's content. The trigger shows it when chosen.
disabledbooleanfalseCannot be chosen. Sets aria-disabled.
selectedbooleanfalseDraws the selected look. Select sets the real selection.
densebooleanfalseLess padding and smaller text.
dividerbooleanfalseA line under the row. Material look only.
textValuestringtext of childrenType-ahead text. Set it for element children.
sx, className, style, refStandard. ref is an HTMLDivElement.

Notes

  • onChange receives a plain SelectChangeEvent, not a DOM event.
  • MenuItem renders a div with role="option", not an li. An id you pass replaces React Aria's generated id.
  • Not supported on Select are input, menu positioning and transition props such as MenuProps.anchorOrigin, and slots.
  • Not supported on MenuItem are autoFocus, component, disableGutters, focusVisibleClassName and href.
  • 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.

On this page