Skip to main content

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.

PropTypeDefaultDescription
valuestring--Controlled selected value
defaultValuestring--Initial value (uncontrolled)
onValueChange(value: string) => void--Called when the selected value changes
openboolean--Controlled open state
defaultOpenbooleanfalseInitial 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').
namestring--Name for native form submission
autoCompletestring--Hint for browser autofill
disabledboolean--Disables the select
requiredboolean--Marks as required for form validation
formstring--Associates with a form element by ID

Select.Trigger

Button that toggles the dropdown. Renders a <button> with role="combobox".

PropTypeDescription
asChildbooleanRender as the child element instead of a <button>
disabledbooleanDisables 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.

PropTypeDefaultDescription
placeholderReact.ReactNode''Content shown when no value is selected

Select.Icon

Decorative icon next to the value. Renders a <span> with aria-hidden.

PropTypeDescription
asChildbooleanRender as child element

Select.Portal

Renders the dropdown content into a portal.

PropTypeDefaultDescription
containerElement | nulldocument.bodyPortal target

Select.Content

The dropdown content. Handles focus trapping, dismiss-on-click-outside, escape-to-close, and keyboard navigation.

PropTypeDefaultDescription
position'item-aligned' | 'popper''item-aligned'Positioning strategy
side'top' | 'right' | 'bottom' | 'left''bottom'Preferred side (popper only)
sideOffsetnumber--Main-axis offset (popper only)
align'start' | 'center' | 'end''start'Cross-axis alignment (popper only)
alignOffsetnumber--Cross-axis offset (popper only)
avoidCollisionsbooleantrueFlip to avoid viewport overflow (popper only)
collisionBoundaryElement | Element[]--Custom collision boundary (popper only)
collisionPaddingnumber10Padding from viewport edges
arrowPaddingnumber0Minimum padding between arrow and content edges (popper only)
sticky'partial' | 'always''partial'Keep content in view when trigger is partially hidden (popper only)
hideWhenDetachedbooleanfalseHide 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.

PropTypeDescription
noncestringNonce 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".

PropTypeDefaultDescription
valuestring(required)The value for this item. Must not be empty string.
disabledbooleanfalseDisables the item
textValuestring--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".

PropTypeDefaultDescription
widthnumber10Arrow width in pixels
heightnumber5Arrow height in pixels

Keyboard interactions

KeyAction
Space / EnterOpens the select (on trigger) or selects focused item
ArrowDownOpens the select or moves focus to next item
ArrowUpOpens the select or moves focus to previous item
HomeMoves focus to first item
EndMoves focus to last item
EscapeCloses the select
TabPrevented while open (standard select behavior)
Type charactersTypeahead search -- focuses matching items