EQTY Lab Equality

Select

Dropdown for choosing one option from a list, with optional search

View as Markdown

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:

tsx
import {
  Select,
  SelectTrigger,
  SelectValue,
  SelectContent,
  SelectItem,
} from "@eqtylab/equality";

Compose a controlled select. SelectValue’s placeholder shows before a choice is made:

tsx
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:

ts
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.

tsx
<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.

tsx
<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.

tsx
<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

tsx
import { ELEVATION } from "@eqtylab/equality";

<SelectContent elevation={ELEVATION.RAISED}>…</SelectContent>;

Slots

NameDescription
SelectThe root that manages selection state. Owns value/onValueChange.
SelectTriggerThe button that opens the dropdown and displays the value.
SelectValueRenders the selected value, or a placeholder when nothing is chosen.
SelectContentThe dropdown popover holding the options. Owns elevation.
SelectItemA selectable option. Requires a value.
SelectGroupGroups related items.
SelectLabelA heading for a group of items.
SelectSeparatorA divider between items or groups.
SelectSearchOptional 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

NameDescriptionTypeDefaultRequired
valueThe selected value (controlled)string—❌
defaultValueThe initial selected value (uncontrolled)string—❌
onValueChangeCalled with the new value when the selection changes(value: string) => void—❌
openWhether the list is open (controlled); opening clears any queryboolean—❌
disabledDisables the entire controlbooleanfalse❌

SelectValue

NameDescriptionTypeDefaultRequired
placeholderText shown before a value is selectedstring—❌

SelectContent

NameDescriptionTypeDefaultRequired
elevationPosition of the dropdown on the elevation scalesunken, base, raised, overlayoverlay❌

SelectItem

NameDescriptionTypeDefaultRequired
valueThe value this item representsstring—✅
disabledDisables just this optionbooleanfalse❌
textValueText used for type-ahead and search matching; defaults to the rendered textstring—❌

SelectSearch

Also accepts standard input attributes, except value and onChange, which are managed internally, and role, which stays searchbox.

NameDescriptionTypeDefaultRequired
alwaysVisibleShow the input on open; false reveals it on the first keystroke insteadbooleantrue❌
placeholderPlaceholder text for the inputstringSearch...❌
iconCustom leading icon; defaults to a search iconReactNode—❌
aria-labelAccessible name for the input; defaults to the placeholder textstringplaceholder❌
refForwarded to the underlying <input>; null while the input is not renderedRef<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.