Select
When to use
Section titled “When to use”- Picking one option from many in a form: country, language
- A compact control where space is short
- A choice that should use the system picker on mobile
Not when people search the list by typing or pick several — use a Combobox — or for up to five options that are worth seeing — use a RadioGroup.
Choosing between choosing one option or a value:
| Component | Use it for | For example |
|---|---|---|
| RadioGroup | Up to about five options, all visible at once, as a form value | Shipping speed, plan size |
| Select | One option from a longer list, in little space, with the system picker | Country, time zone, sort order in a form |
| Combobox | A long list people search by typing, several picks, or options loaded from a server | Assign reviewers, search a city, add tags |
| ToggleGroup | A few options shown as buttons that apply at once, not a form field | Text alignment, list or grid view |
| Slider | A number in a range, where dragging is easier than typing an exact value | Volume, brightness, price range |
Select is a real <select>. The picker, keyboard, typeahead, validation and form submission are
all the platform’s, so there is no headless half. On phones it opens the system picker. It works
with react-hook-form’s register() as-is.
import { Select } from '@arun-dev/ui';
const field = { display: 'grid', gap: 'var(--space-3xs)', maxInlineSize: '20rem' };
export default function SelectBasics() { return ( <> {/* The empty-value option is the prompt: required rejects it, and it reads muted. */} <div style={field}> <label htmlFor="select-country">Country</label> <Select id="select-country" required defaultValue=""> <option value="" disabled> Choose a country… </option> <option value="in">India</option> <option value="jp">Japan</option> <option value="gb">United Kingdom</option> <option value="us">United States</option> </Select> </div>
<div style={field}> <label htmlFor="select-plan">Plan</label> <Select id="select-plan" defaultValue="" aria-invalid aria-describedby="select-plan-error"> <option value="">Choose a plan…</option> <optgroup label="Monthly"> <option value="starter-monthly">Starter</option> <option value="team-monthly">Team</option> </optgroup> <optgroup label="Yearly"> <option value="starter-yearly">Starter</option> <option value="team-yearly">Team</option> </optgroup> </Select> <small id="select-plan-error" style={{ color: 'var(--color-status-error)' }}> Pick a plan to continue. </small> </div>
<div style={field}> <label htmlFor="select-disabled">Disabled</label> <Select id="select-disabled" defaultValue="read" disabled> <option value="read">Read only</option> <option value="write">Read and write</option> </Select> </div> </> );}Playground
Section titled “Playground”<Select>{options}</Select>Two elements, one rule
Section titled “Two elements, one rule”Like Input, the <select> sits inside a <div class="select"> that draws
the border and the chevron:
| Prop | Lands on |
|---|---|
className | the box |
children — your <option>s and <optgroup>s | the <select> |
everything else, including id, ref, render | the <select> |
The chevron is drawn by the box rather than the browser, so it looks the same everywhere, and
clicks on it fall through to the <select>.
A placeholder
Section titled “A placeholder”<select> has no placeholder. Use an option with an empty value instead: while it is
selected, the field text is muted, and required refuses to submit it. Add disabled to it so it
can’t be picked again once a real choice is made.
<Select required defaultValue=""> <option value="" disabled> Choose a country… </option> …</Select>Search
Section titled “Search”There is no search box. A native <select> can’t hold one, and building one would give up the
system picker and everything else above. What it does have is typeahead: with the field
focused, type the start of an option to jump to it.
That covers short lists well. For long lists where users need to filter, a combobox is the right component; it will come after Popover, since it needs a positioned popup.
Multiple
Section titled “Multiple”multiple, or a size above 1, turns the select into a list box: the options show in place, the
chevron goes, and the field sets its own height.
import { Select } from '@arun-dev/ui';
const field = { display: 'grid', gap: 'var(--space-3xs)', maxInlineSize: '20rem' };
export default function SelectMultiple() { return ( <div style={field}> <label htmlFor="select-toppings">Toppings</label> {/* multiple turns the select into a list box: no chevron, options shown in place. */} <Select id="select-toppings" multiple size={5} defaultValue={['basil', 'olives']} aria-describedby="select-toppings-hint" > <option value="basil">Basil</option> <option value="mushroom">Mushroom</option> <option value="olives">Olives</option> <option value="onion">Onion</option> <option value="pepper">Pepper</option> <option value="pineapple">Pineapple</option> </Select> <small id="select-toppings-hint" style={{ color: 'var(--color-text-muted)' }}> Hold Ctrl or ⌘ to pick more than one. </small> </div> );}Consider checkboxes
A multi-select list box needs Ctrl or ⌘ to pick more than one, which many users don’t know. For a handful of options, a set of Checkboxes is easier to use.
States
Section titled “States”All of them come from the native control, read with :has(). There is no data-* attribute
to set:
| State | Set it with |
|---|---|
| focus | the platform (:focus-visible) |
| invalid | aria-invalid on the select |
| disabled | disabled on the select |
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | The <option> and <optgroup> elements, as for a native <select>. An option with value="" selected reads as a placeholder. | |
className | string | Goes on the box that frames the select and its chevron, not on the <select>. Every other prop, ref and render included, goes on the <select>. | |
render | ReactElement | Element or component to render instead of the default. Props, className, event handlers and ref are merged onto it. | |
ref | Ref<HTMLElement> | Ref to the rendered element. Merged with any ref on the render element. | |
…rest | HTMLAttributes | Anything else is spread onto the rendered element, so id, aria-*, data-* and event handlers all reach the DOM. |
- Controlled use works as with any
<select>:valueandonChange, whereonChangefires as soon as the choice changes. - The open list is drawn by the browser and operating system, so its look varies by platform. Only the closed field is styled.
- Restyle every select with the
--select-*tokens, e.g.--select-heightor--select-icon-color.