Select
Dropdown select with search, multiple selection, card variant, and controlled keyboard integration points. 8 composable parts.
Installation
dgadd is not public on npm yet. Until the first release, pack @diffgazer/add from the repository and install that tarball into this app, which is what puts dgadd on pnpm exec.
The hosted registry is not public yet because r.b4r7.dev does not resolve. Use this source checkout or a local registry preview until the endpoint returns 200.
Diffgazer packages are not yet published to npm. Until the first release, pack @diffgazer/ui and @diffgazer/keys from the repository and install those tarballs.
UI components require Tailwind CSS v4. Local copy mode imports src/styles/styles.css; package mode uses @diffgazer/ui CSS once packages are available.
Usage
Examples
Open Listbox (defaultOpen)
Disabled and Invalid
Searchable (bottom)
Searchable (top)
Multiple
Display Modes
Avatar Picker (render children)
Tags (Multiselect)
Radio Style
Card Variant
API Reference
Select
SelectTrigger
SelectContent
SelectValue
SelectTags
SelectSearch
SelectItem
SelectEmpty
Data attributes
Accessibility
Keyboard Navigation
SelectTrigger follows the closed combobox key map. SelectContent handles listbox navigation and restores focus to the trigger on Escape and Tab.
| Key | Action |
|---|---|
| Enter / Space | On the trigger, toggles the listbox. In the listbox, selects the highlighted option. |
| ArrowDown / ArrowUp | Opens the listbox from the trigger, then moves highlight through enabled options. |
| Home / End | On a closed non-searchable trigger, opens and highlights the first or last enabled option. In the listbox, jumps to the first or last enabled option. |
| Printable character | On a closed non-searchable trigger, opens and typeaheads to the matching option. In an open non-searchable listbox, typeahead updates the highlight. |
| Tab | Closes the open listbox and synchronously restores focus to the trigger; single-select commits the highlighted option first. |
| Escape | Closes the open listbox and returns focus to the trigger. |
| Search input ArrowDown / ArrowUp | Moves the active descendant through filtered enabled options when SelectSearch is focused. |
| Search input Enter | Selects the active filtered option when one is visible. |
Searchable with keyboard navigation
Notes
Requires @diffgazer/keys (package mode)
Select's built-in keyboard navigation imports from the required @diffgazer/keys peer. Package consumers must install @diffgazer/keys with @diffgazer/ui; packages are not yet published to npm, so until the first release install both from locally packed tarballs. Importing @diffgazer/ui/components/select without keys fails at module load with an error naming the missing @diffgazer/keys package. Copy/dgadd consumers do not need the package — copy mode rewrites the keyboard hooks to local source.
Composition Contract
Use Select.Trigger, Select.Content, Select.Item, and the other Select parts as explicit children in the Select JSX tree. Custom option UI belongs inside Select.Item, with textValue when the visible content is not plain text. Components that create items internally from an opaque wrapper are not part of the current public contract.
Card Variant
Set variant='card' on Select for a settings-panel layout with checkbox-style indicators and a thick border treatment. The list still respects open/defaultOpen — pass defaultOpen to render the inline list immediately, or pair with open/onOpenChange to control expansion. The card trigger is an inverted --foreground header, so it takes its keyboard focus indicator as an outside outline (outline-2 outline-ring outline-offset-0) drawn on the page background; the default trigger keeps the inset Input focus ring.
Multiple Selection
Set multiple={true} to enable multi-select. Value becomes string[] instead of string. Items show [x]/[ ] checkbox indicators in multiple mode by default. The dropdown stays open after selection.
Item Indicators
Control the visual indicator on SelectItem via the indicator prop: 'auto' (default, [x]/[ ] in multi, ✓ in single), 'checkbox' (always [x]/[ ]), 'radio' ([ • ]/[ ] radio-style), or 'none' (text-only, no prefix). Use 'none' for tag-style multiselect, 'radio' for single-choice lists.
SelectTags vs SelectValue
Use SelectTags inside SelectTrigger for multiselect. It renders each selected item as an outlined chip showing the option label, and its placeholder accepts a string. Use SelectValue for single-select or multiselect with display modes: 'count' (default, 'N selected'), 'list' (comma-separated), or 'truncate' (first N + '+M more'). Its placeholder accepts ReactNode.
Search Position
SelectSearch can go anywhere inside SelectContent — its position prop, not its JSX order, decides where it renders. position='bottom' (default) renders the search row below the option list with a top border; position='top' renders it above the list with a bottom border. DOM and Fragment wrappers around SelectSearch or SelectEmpty are preserved when those parts are hoisted outside the listbox.
Searchable APG semantics
When SelectSearch is present, Select implements the WAI-ARIA Editable Combobox With List Autocomplete pattern: the search input is the combobox (role='combobox', aria-controls -> listbox, aria-activedescendant on focused option, aria-autocomplete='list'). The trigger button reduces to a simple toggle (aria-haspopup='listbox' + aria-expanded). When no SelectSearch is present, the trigger button itself is the combobox following the Combobox With Listbox Popup pattern. This avoids splitting combobox state across two controls.
Empty State
Add SelectEmpty inside SelectContent to show a message after a nonempty search query matches no items. It renders nothing before a query is entered, including when the option list is empty. Default: '> no results.' Pass children to customize.
Keyboard Navigation
SelectContent includes built-in keyboard navigation via useNavigation: ArrowUp/Down to move, Enter/Space to select, Home/End to jump, Tab commits the highlighted single-select option and closes, and Escape closes. Highlight state is exposed via highlighted/onHighlightChange props on Select for external integration.
Custom Trigger Content
For advanced customization, pass a render function as children to SelectValue: <SelectValue>{({ selected, labels }) => selected.map((value) => labels.get(value) ?? value).join(', ')}</SelectValue>. Use the public Select parts for custom trigger and value layouts.
Keyboard focus on the trigger is variant-owned. The default trigger uses the inset field ring shared with Input (focus:border-ring focus:ring-1 focus:ring-ring). The card trigger is an inverted --foreground header, where an inset --ring resolves to the same value as the surface it sits on and disappears — so it draws its indicator outside instead (focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-0), on the page background where it keeps full contrast in both themes.
The card-variant selected-state indicator pulses via the motion-safe:animate-pulse Tailwind class. The motion-safe: prefix gates the animation behind the user's prefers-reduced-motion setting — the class is part of the public CSS contract for the card variant. When prefers-reduced-motion: reduce is active, the pulse never plays.
Source
Install via CLI: pnpm exec dgadd add ui/select. Keyboard hooks are included as standalone copies. For the full experience, use --integration keys.
Highlighted source loads after this disclosure opens. Browse the source repository.