Select
Choose one option from a styled list.
import { Select } from "@merid/react";
export function Example() {
return (
<>
<span id="region-label">Region</span>
<Select.Root defaultValue="eu-west" name="region">
<Select.Trigger aria-labelledby="region-label" />
<Select.Content>
<Select.Item value="us-east">US East</Select.Item>
<Select.Item value="us-west">US West</Select.Item>
<Select.Item value="eu-west">EU West</Select.Item>
<Select.Item value="ap-south" disabled>
Asia Pacific (soon)
</Select.Item>
</Select.Content>
</Select.Root>
</>
);
}Import
import { Select } from "@merid/react";Anatomy
<Select.Root>
<Select.Trigger />
<Select.Content>
<Select.Item value="a">A</Select.Item>
</Select.Content>
</Select.Root>- Root holds the value and open state, and renders a hidden
<input>whennameis set. - Trigger is the
role="combobox"button. It shows the selected label or the placeholder. - Content is the portalled
role="listbox". It stays mounted (hidden) so options can report their labels. - Item is one
role="option". Its text children are the label, shown in the trigger and used for typeahead.
Examples
Controlled
Pass value and onValueChange. An empty string means nothing is selected and shows the placeholder.
const [plan, setPlan] = useState("");
<Select.Root value={plan} onValueChange={setPlan} placeholder="Choose a plan">
<Select.Trigger aria-label="Plan" invalid={plan === ""} />
<Select.Content>
<Select.Item value="starter">Starter</Select.Item>
<Select.Item value="team">Team</Select.Item>
<Select.Item value="enterprise">Enterprise</Select.Item>
</Select.Content>
</Select.Root>Sizes and disabled
<Select.Trigger size="sm" aria-label="Frequency" />
<Select.Trigger size="md" aria-label="Frequency" />
<Select.Trigger size="lg" aria-label="Frequency" />
<Select.Root disabled placeholder="Disabled">
<Select.Trigger aria-label="Disabled select" />
<Select.Content>
<Select.Item value="x">Unavailable</Select.Item>
</Select.Content>
</Select.Root>In a form
Set name (and optionally required and form) on Root. A hidden input carries the value on submit.
<form>
<Select.Root name="region" required>
<Select.Trigger aria-label="Region" />
<Select.Content>…</Select.Content>
</Select.Root>
</form>API reference
Root
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | – | Controlled selected value ("" means nothing selected). |
defaultValue | string | "" | Initial value when uncontrolled. |
onValueChange | (value: string) => void | – | Called with the newly selected value. |
open | boolean | – | Controlled open state. |
defaultOpen | boolean | false | Initial open state when uncontrolled. |
onOpenChange | (open: boolean) => void | – | Called when the open state should change. |
name | string | – | Form field name; renders a hidden input carrying the value. |
required | boolean | – | Marks the hidden input as required. |
disabled | boolean | false | Disable the whole select. |
placeholder | ReactNode | "Select…" | Shown in the trigger when no value is selected. |
form | string | – | Associates the hidden input with a form by id. |
children | ReactNode | – | Trigger and Content. |
Trigger
Accepts every <button> attribute except value. Children, when given, replace the displayed label. An id you pass is kept (so <label htmlFor> works) and the listbox stays labelled by it. Inside a Field the trigger picks up the wiring like every Merid control: the field's id and label (aria-labelledby), aria-describedby for the description and error, aria-invalid + data-invalid, aria-required and disabled. Explicit props win.
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "md" | "lg" | "md" | Control size. |
invalid | boolean | – | Marks the field invalid: `aria-invalid` for semantics, `data-invalid` for the danger border. Inside a Field, derived from its error. |
ref | Ref<HTMLButtonElement> | – | Forwarded ref to the combobox button. |
Content
Accepts every <div> attribute. The listbox matches the trigger's width.
| Prop | Type | Default | Description |
|---|---|---|---|
placement | Placement | "bottom-start" | Preferred placement. |
sideOffset | number | 6 | Distance from the trigger in px. |
container | Element | null | document.body | Portal target. `undefined` uses `document.body`; `null` renders nothing until the target exists. |
ref | Ref<HTMLDivElement> | – | Forwarded ref to the listbox. |
Item
Accepts every <div> attribute except children. Your onClick and onPointerMove run first; call event.preventDefault() in them to skip selecting or highlighting the option.
| Prop | Type | Default | Description |
|---|---|---|---|
valueRequired | string | – | Value submitted and reported by onValueChange. |
childrenRequired | string | – | Visible label. Also used in the trigger and for typeahead. |
disabled | boolean | false | Disable the option. |
Styling
| Class | Element |
|---|---|
.mrd-select__trigger | Trigger |
.mrd-select__value, .mrd-select__icon | Trigger label and chevron |
.mrd-select__content | Listbox |
.mrd-select__item, .mrd-select__item-text, .mrd-select__check | Option, its text, the selected check |
| Attribute | On | Values |
|---|---|---|
data-state | Trigger, Content | open, closed |
data-size | Trigger | sm, md, lg |
data-placeholder | Trigger | present when no value is selected |
data-invalid | Trigger | present when invalid (drives the danger border) |
data-active | Item | present on the keyboard/pointer-highlighted option |
data-disabled | Item | present when disabled |
aria-selected="true" | Item | the selected option |
The trigger height comes from --mrd-select-height, set per data-size.
Accessibility
Follows the WAI-ARIA Select-Only Combobox pattern. Focus stays on the trigger; the highlighted option is exposed through aria-activedescendant. Give the trigger an accessible name with aria-label, aria-labelledby, or an id referenced by a <label htmlFor>.
| Key | Action |
|---|---|
| Enter | Open, or select the highlighted option and close. |
| Space | Open, or select the highlighted option and close. |
| ArrowDown | Open, or highlight the next option. |
| ArrowUp | Open, or highlight the previous option. |
| Home | Highlight the first option. |
| End | Highlight the last option. |
| A–Z | Typeahead. When closed, selects the match directly. |
| Tab | Select the highlighted option, close and move focus on. |
| Escape | Close without changing the value. |
Guidelines
Do
Use Select for 5–15 options where the list itself is familiar (countries, regions, plans).
Avoid
Use Select for two or three options — show them as Radio or a SegmentedControl.
Do
Always pair the trigger with a visible label.
Avoid
Rely on the placeholder as the only label; it disappears once a value is chosen.
Related
- NativeSelect — the platform
<select>, best on mobile and for long lists. - DropdownMenu — actions, not values.
- Field — label, hint and error around a control.