Select
Dropdown for choosing one option from a list, with optional search
Overview
Select lets users choose a single option from a dropdown list. It’s built on Radix UI Select , so it’s fully keyboard accessible (type-ahead, arrow keys, ), manages focus, and exposes the right roles to assistive technology. Long lists scroll within the popover, and can opt in to in-place search .
It is a compound component: a Select root wraps a SelectTrigger (showing the current SelectValue) and a SelectContent holding the SelectItems. Use it for choosing from a moderate set of options; for free-text entry use an Input , and for a lightweight filter with counts use a Radio Dropdown .
Usage
Import the parts you need:
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@eqtylab/equality";Compose a controlled select. SelectValue’s placeholder shows before a choice is made:
const [value, setValue] = useState("");
<Select value={value} onValueChange={setValue}>
<SelectTrigger id="fruit">
<SelectValue placeholder="Select an option" />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="orange">Orange</SelectItem>
<SelectItem value="pear">Pear</SelectItem>
</SelectContent>
</Select>;Select can be controlled (value + onValueChange) or uncontrolled (defaultValue). Pair the trigger with a Label via id/htmlFor for an accessible caption.
Variants
Default
Disabled
Set disabled on the Select root to disable the whole control.
Pre-selected
Use defaultValue on the root to start with an option chosen. Long lists scroll within the popover.
Search & filtering
Add a SelectSearch inside SelectContent to filter options in place. It is opt-in per select — without it, the select behaves exactly as before and the built-in type-ahead still works. It works like Dropdown Menu search , except that the search box is visible by default: options that don’t match are hidden, and matching is a case-insensitive substring of each item’s textValue, falling back to its rendered text.
Reach for search whenever the list can grow long enough to scroll — picking from dozens of options by scrolling or type-ahead alone is slow. Search only filters the options you provide; it doesn’t accept free-text values.
Import the additional parts:
import {
SelectSearch,
SelectEmpty,
SelectGroup,
SelectLabel,
SelectSeparator,
} from "@eqtylab/equality";Default
The search box is shown, focused, the moment the select opens, so mouse and screen reader users can see that search exists. Labels, separators and groups with no matches hide while a search is active so results stay compact, and the chosen value stays in the trigger even when a query hides its option. Add a SelectEmpty to show a “no results” row when nothing matches.
<SelectContent>
<SelectSearch placeholder="Search countries..." />
<SelectGroup>
<SelectLabel>Americas</SelectLabel>
<SelectItem value="argentina">Argentina</SelectItem>
{/* ...more countries... */}
</SelectGroup>
<SelectSeparator />
<SelectGroup>{/* ...another region... */}</SelectGroup>
<SelectEmpty>No countries found</SelectEmpty>
</SelectContent>Reveal on typing
Pass alwaysVisible={false} to keep the search box hidden until someone types into the open list — the first keystroke reveals it and seeds the query. Nothing on screen says search exists, so keep this for compact, keyboard-heavy UIs. Typing on the closed trigger still uses Radix type-ahead and changes the value directly; search only starts once the list is open.
<SelectContent>
<SelectSearch alwaysVisible={false} placeholder="Search countries..." />
{/* ...items... */}
<SelectEmpty>No countries found</SelectEmpty>
</SelectContent>Items with rich content
Give items that lead with an icon, flag or avatar a textValue, so they match on the label rather than on everything they render.
<SelectItem value="japan" textValue="Japan">
<Icon icon="Flag" /> Japan
</SelectItem>Keyboard
in the search box selects the first matching option. moves from the search box to the first option and to the last; on the first option returns to the search box. Typing or while an option is focused keeps editing the query.
Accessibility
The search box and its live regions sit inside the list, which Radix gives the listbox role. ARIA only allows options and groups there, so automated checkers such as axe report aria-required-children, and some screen readers skip the search box in browse mode. They can’t move outside the list: while the select is open, Radix hides everything outside it from assistive technology. Keyboard use and announcements work as described above.
Elevations
Set the elevation prop on SelectContent to place the dropdown on the elevation scale. overlay is the default.
Sunken
Base
Raised
Overlay (default)
Usage
import { ELEVATION } from "@eqtylab/equality";
<SelectContent elevation={ELEVATION.RAISED}>…</SelectContent>;Slots
| Name | Description |
|---|---|
Select | The root that manages selection state. Owns value/onValueChange. |
SelectTrigger | The button that opens the dropdown and displays the value. |
SelectValue | Renders the selected value, or a placeholder when nothing is chosen. |
SelectContent | The dropdown popover holding the options. Owns elevation. |
SelectItem | A selectable option. Requires a value. |
SelectGroup | Groups related items. |
SelectLabel | A heading for a group of items. |
SelectSeparator | A divider between items or groups. |
SelectSearch | Optional search input that filters items in place. |
SelectEmpty | ”No results” row shown only when a search matches nothing. |
Props
The parts forward their Radix Select props (and className). The most commonly used are below.
Select
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
value | The selected value (controlled) | string | — | ❌ |
defaultValue | The initial selected value (uncontrolled) | string | — | ❌ |
onValueChange | Called with the new value when the selection changes | (value: string) => void | — | ❌ |
open | Whether the list is open (controlled); opening clears any query | boolean | — | ❌ |
disabled | Disables the entire control | boolean | false | ❌ |
SelectValue
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
placeholder | Text shown before a value is selected | string | — | ❌ |
SelectContent
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
elevation | Position of the dropdown on the elevation scale | sunken, base, raised, overlay | overlay | ❌ |
SelectItem
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
value | The value this item represents | string | — | ✅ |
disabled | Disables just this option | boolean | false | ❌ |
textValue | Text used for type-ahead and search matching; defaults to the rendered text | string | — | ❌ |
SelectSearch
Also accepts standard input attributes, except value and onChange, which are managed internally, and role, which stays searchbox.
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
alwaysVisible | Show the input on open; false reveals it on the first keystroke instead | boolean | true | ❌ |
placeholder | Placeholder text for the input | string | Search... | ❌ |
icon | Custom leading icon; defaults to a search icon | ReactNode | — | ❌ |
aria-label | Accessible name for the input; defaults to the placeholder text | string | placeholder | ❌ |
ref | Forwarded to the underlying <input>; null while the input is not rendered | Ref<HTMLInputElement> | — | ❌ |
SelectEmpty
Renders its children as a “no results” message, shown only while a search query matches no items. It is a live region (role="status"), so the message is announced when filtering empties the list. Also accepts standard div attributes.