Select
An accessible select/dropdown with keyboard navigation, typeahead search, scroll buttons, and form integration.
import * as Select from '@gentleduck/primitives/select'import * as Select from '@gentleduck/primitives/select'Anatomy
<Select.Root>
<Select.Trigger>
<Select.Value />
<Select.Icon />
</Select.Trigger>
<Select.Portal>
<Select.Content>
<Select.ScrollUpButton />
<Select.Viewport>
<Select.Group>
<Select.Label />
<Select.Item>
<Select.ItemText />
<Select.ItemIndicator />
</Select.Item>
</Select.Group>
<Select.Separator />
</Select.Viewport>
<Select.ScrollDownButton />
</Select.Content>
</Select.Portal>
</Select.Root><Select.Root>
<Select.Trigger>
<Select.Value />
<Select.Icon />
</Select.Trigger>
<Select.Portal>
<Select.Content>
<Select.ScrollUpButton />
<Select.Viewport>
<Select.Group>
<Select.Label />
<Select.Item>
<Select.ItemText />
<Select.ItemIndicator />
</Select.Item>
</Select.Group>
<Select.Separator />
</Select.Viewport>
<Select.ScrollDownButton />
</Select.Content>
</Select.Portal>
</Select.Root>Example
import * as Select from '@gentleduck/primitives/select'
function ThemeSelect() {
return (
<Select.Root defaultValue="system">
<Select.Trigger className="px-3 py-2 border rounded">
<Select.Value placeholder="Pick a theme" />
<Select.Icon />
</Select.Trigger>
<Select.Portal>
<Select.Content position="popper" sideOffset={4} className="bg-white border rounded shadow-lg">
<Select.Viewport className="p-1">
<Select.Item value="light" className="px-3 py-1.5 rounded cursor-pointer hover:bg-gray-100">
<Select.ItemText>Light</Select.ItemText>
<Select.ItemIndicator>*</Select.ItemIndicator>
</Select.Item>
<Select.Item value="dark" className="px-3 py-1.5 rounded cursor-pointer hover:bg-gray-100">
<Select.ItemText>Dark</Select.ItemText>
<Select.ItemIndicator>*</Select.ItemIndicator>
</Select.Item>
<Select.Item value="system" className="px-3 py-1.5 rounded cursor-pointer hover:bg-gray-100">
<Select.ItemText>System</Select.ItemText>
<Select.ItemIndicator>*</Select.ItemIndicator>
</Select.Item>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
)
}import * as Select from '@gentleduck/primitives/select'
function ThemeSelect() {
return (
<Select.Root defaultValue="system">
<Select.Trigger className="px-3 py-2 border rounded">
<Select.Value placeholder="Pick a theme" />
<Select.Icon />
</Select.Trigger>
<Select.Portal>
<Select.Content position="popper" sideOffset={4} className="bg-white border rounded shadow-lg">
<Select.Viewport className="p-1">
<Select.Item value="light" className="px-3 py-1.5 rounded cursor-pointer hover:bg-gray-100">
<Select.ItemText>Light</Select.ItemText>
<Select.ItemIndicator>*</Select.ItemIndicator>
</Select.Item>
<Select.Item value="dark" className="px-3 py-1.5 rounded cursor-pointer hover:bg-gray-100">
<Select.ItemText>Dark</Select.ItemText>
<Select.ItemIndicator>*</Select.ItemIndicator>
</Select.Item>
<Select.Item value="system" className="px-3 py-1.5 rounded cursor-pointer hover:bg-gray-100">
<Select.ItemText>System</Select.ItemText>
<Select.ItemIndicator>*</Select.ItemIndicator>
</Select.Item>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
)
}API
Select.Root
The root component that manages open/closed state, selected value, and provides context to all children. Also renders a hidden native <select> for form compatibility.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | -- | Controlled selected value |
defaultValue | string | -- | Initial value (uncontrolled) |
onValueChange | (value: string) => void | -- | Called when the selected value changes |
open | boolean | -- | Controlled open state |
defaultOpen | boolean | false | Initial open state (uncontrolled) |
onOpenChange | (open: boolean) => void | -- | Called when the open state should change |
dir | 'ltr' | 'rtl' | -- | Text direction. Resolved with useDirection (dir prop -> DirectionProvider -> 'ltr'). |
name | string | -- | Name for native form submission |
autoComplete | string | -- | Hint for browser autofill |
disabled | boolean | -- | Disables the select |
required | boolean | -- | Marks as required for form validation |
form | string | -- | Associates with a form element by ID |
Select.Trigger
Button that toggles the dropdown. Renders a <button> with role="combobox".
| Prop | Type | Description |
|---|---|---|
asChild | boolean | Render as the child element instead of a <button> |
disabled | boolean | Disables the trigger |
Sets aria-expanded, aria-controls, aria-autocomplete="none", data-state, and data-placeholder automatically.
Select.Value
Displays the selected item text or a placeholder.
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder | React.ReactNode | '' | Content shown when no value is selected |
Select.Icon
Decorative icon next to the value. Renders a <span> with aria-hidden.
| Prop | Type | Description |
|---|---|---|
asChild | boolean | Render as child element |
Select.Portal
Renders the dropdown content into a portal.
| Prop | Type | Default | Description |
|---|---|---|---|
container | Element | null | document.body | Portal target |
Select.Content
The dropdown content. Handles focus trapping, dismiss-on-click-outside, escape-to-close, and keyboard navigation.
| Prop | Type | Default | Description |
|---|---|---|---|
position | 'item-aligned' | 'popper' | 'item-aligned' | Positioning strategy |
side | 'top' | 'right' | 'bottom' | 'left' | 'bottom' | Preferred side (popper only) |
sideOffset | number | -- | Main-axis offset (popper only) |
align | 'start' | 'center' | 'end' | 'start' | Cross-axis alignment (popper only) |
alignOffset | number | -- | Cross-axis offset (popper only) |
avoidCollisions | boolean | true | Flip to avoid viewport overflow (popper only) |
collisionBoundary | Element | Element[] | -- | Custom collision boundary (popper only) |
collisionPadding | number | 10 | Padding from viewport edges |
arrowPadding | number | 0 | Minimum padding between arrow and content edges (popper only) |
sticky | 'partial' | 'always' | 'partial' | Keep content in view when trigger is partially hidden (popper only) |
hideWhenDetached | boolean | false | Hide content when trigger is fully occluded (popper only) |
onCloseAutoFocus | (event: Event) => void | -- | Called when focus returns to trigger on close |
onEscapeKeyDown | (event: KeyboardEvent) => void | -- | Called when Escape is pressed |
onPointerDownOutside | (event) => void | -- | Called when clicking outside |
Exposes data-state="open" / data-state="closed" and data-side for CSS animation.
When using position="popper", the following CSS custom properties are available:
--gentleduck-select-content-transform-origin--gentleduck-select-content-available-width--gentleduck-select-content-available-height--gentleduck-select-trigger-width--gentleduck-select-trigger-height
Select.Viewport
Scrollable container for items inside the content.
| Prop | Type | Description |
|---|---|---|
nonce | string | Nonce for the injected scrollbar-hiding style element |
Select.Group
Groups related items. Renders a <div> with role="group".
Select.Label
Label for a group. Linked via aria-labelledby.
Select.Item
A selectable item. Renders a <div> with role="option".
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | (required) | The value for this item. Must not be empty string. |
disabled | boolean | false | Disables the item |
textValue | string | -- | Text override for typeahead search (defaults to text content) |
Exposes data-state="checked" / data-state="unchecked", data-highlighted, and data-disabled.
Select.ItemText
The text portion of an item. When selected, the text content is portalled into the Value component.
Select.ItemIndicator
Renders only when the parent item is selected. Use for check marks or other visual indicators.
Select.ScrollUpButton / Select.ScrollDownButton
Scroll buttons that appear when content overflows. Auto-scroll on hover.
Select.Separator
Visual separator between items. Renders a <div> with aria-hidden.
Select.Arrow
Arrow pointing to the trigger. Only renders when position="popper".
| Prop | Type | Default | Description |
|---|---|---|---|
width | number | 10 | Arrow width in pixels |
height | number | 5 | Arrow height in pixels |
Keyboard interactions
| Key | Action |
|---|---|
| Space / Enter | Opens the select (on trigger) or selects focused item |
| ArrowDown | Opens the select or moves focus to next item |
| ArrowUp | Opens the select or moves focus to previous item |
| Home | Moves focus to first item |
| End | Moves focus to last item |
| Escape | Closes the select |
| Tab | Prevented while open (standard select behavior) |
| Type characters | Typeahead search -- focuses matching items |