Skip to main content

Select

  • Component overview: Single-selection dropdown with optional trigger-input search filtering. The public API uses a declarative options configuration, and there is no arrow icon on the trigger per the Figma spec.
  • Size baseline: Trigger min-height 40px, horizontal padding 16px, border radius 5px. Menu item padding 16px × 8px.
  • Implementation note: ui/select retains structural skeleton with unstyledVisual; the design layer owns border, color, shadow, and state visuals for all sub-components.
  • Figma spec

Basic Usage

Declarative options API — the simplest way to render a select. Set searchable to turn the trigger into a searchable input and filter the dropdown as you type.

Result
Loading...
Live Editor
render(
  <div style={{ width: 280 }}>
    <Select
      placeholder="Select a member role"
      options={[
        { value: 'owner', label: 'Owner' },
        { value: 'admin', label: 'Admin' },
        { value: 'editor', label: 'Editor' },
        { value: 'viewer', label: 'Viewer' },
        { value: 'commenter', label: 'Commenter' },
        { value: 'guest', label: 'Guest' },
        { value: 'billing', label: 'Billing' },
        { value: 'auditor', label: 'Auditor' },
        { value: 'developer', label: 'Developer' },
        { value: 'support', label: 'Support' },
      ]}
    />
  </div>,
)

Grouped Options

Use type: 'group' and type: 'separator' for categorized menus.

Result
Loading...
Live Editor
render(
  <div style={{ width: 280 }}>
    <Select
      placeholder="Select a team"
      options={[
        {
          type: 'group',
          label: 'Workspace',
          options: [
            { value: 'design', label: 'Design' },
            { value: 'engineering', label: 'Engineering' },
          ],
        },
        { type: 'separator' },
        {
          type: 'group',
          label: 'Status',
          options: [
            { value: 'active', label: 'Active' },
            { value: 'pending', label: 'Pending', disabled: true },
          ],
        },
      ]}
    />
  </div>,
)

States

Trigger states aligned with Figma variant matrix: State × Filled × Status.

Result
Loading...
Live Editor
render(
  <div
    style={{
      display: 'grid',
      gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
      gap: 16,
      maxWidth: 760,
    }}
  >
    <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
      <span style={{ fontSize: 12, color: '#999' }}>Normal / Empty / Default</span>
      <Select
        placeholder="Select"
        options={[
          { value: 'apple', label: 'Apple' },
          { value: 'banana', label: 'Banana' },
        ]}
      />
    </div>
    <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
      <span style={{ fontSize: 12, color: '#999' }}>Normal / Filled / Default</span>
      <Select
        defaultValue="banana"
        placeholder="Select"
        options={[
          { value: 'apple', label: 'Apple' },
          { value: 'banana', label: 'Banana' },
        ]}
      />
    </div>
    <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
      <span style={{ fontSize: 12, color: '#999' }}>Normal / Empty / Error</span>
      <Select
        placeholder="Error state"
        error
        options={[
          { value: 'apple', label: 'Apple' },
          { value: 'banana', label: 'Banana' },
        ]}
      />
    </div>
    <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
      <span style={{ fontSize: 12, color: '#999' }}>Normal / Filled / Error</span>
      <Select
        defaultValue="banana"
        error
        options={[
          { value: 'apple', label: 'Apple' },
          { value: 'banana', label: 'Banana' },
        ]}
      />
    </div>
    <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
      <span style={{ fontSize: 12, color: '#999' }}>Disabled / Empty</span>
      <Select disabled placeholder="Unavailable" options={[{ value: 'apple', label: 'Apple' }]} />
    </div>
    <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
      <span style={{ fontSize: 12, color: '#999' }}>Disabled / Filled</span>
      <Select
        defaultValue="banana"
        disabled
        placeholder="Unavailable"
        options={[
          { value: 'apple', label: 'Apple' },
          { value: 'banana', label: 'Banana' },
        ]}
      />
    </div>
  </div>,
)

Searchable

Set searchable to turn the trigger itself into an input. Typing in the trigger filters the dropdown options in real time.

Result
Loading...
Live Editor
render(
  <div style={{ width: 280 }}>
    <Select
      placeholder="Search and select"
      searchable
      options={[
        { value: 'alice', label: 'Alice Johnson' },
        { value: 'bob', label: 'Bob Smith' },
        { value: 'charlie', label: 'Charlie Brown' },
        { value: 'diana', label: 'Diana Prince' },
        { value: 'eve', label: 'Eve Williams' },
      ]}
    />
  </div>,
)

Trigger Token Table

StateBorderBackgroundNote
DefaultSeparators/Emphasized#CCCCCCtransparentCollapsed
Focused / OpenLabels/Primary#000000transparentExpanded or focused
ErrorStatus/Destructive#FF503Ftransparenterror prop
DisabledSeparators/Emphasized#CCCCCCGrays/Gray-1#EBEBEBdisabled prop
ElementPropertyToken / Value
ContentBackgroundGrays/White#FFFFFF
ContentBorderSeparators/Default#EBEBEB
ContentShadowEffects/Shadow/Defaultrgba(0,0,0,0.1)
ContentBorder radiusRadius_5 (5px)
ContentMin width160px
ContentPadding (vertical)Spacing_8 (8px)
ItemText colorLabels/Secondary#3D3D3D
ItemHover backgroundGrays/Gray-1#EBEBEB
Item (disabled)Text colorLabels/Disabled#A3A3A3
LabelText colorLabels/Tertiary#757575
SeparatorColorSeparators/Non-opaquergba(0,0,0,0.08)

Size Spec

DimensionValue
Trigger min-height40px
Trigger horizontal padding16px
Trigger border radius5px
Trigger fontBody/Regular (14px, line-height 22)
Trigger arrow iconNone (hidden per Figma)
Content min-width160px
Content border radius5px
Content vertical padding8px
Item horizontal padding16px
Item vertical padding8px
Item fontBody/Regular (14px, line-height 22)
Separator horizontal margin16px
Separator vertical margin4px

Props

Select (declarative)

PropTypeDefaultDescription
optionsSelectOptionEntry[]-Declarative option entries
placeholderstring-Placeholder text
valuestring-Controlled value
defaultValuestring-Initial value (uncontrolled)
searchablebooleanfalseEnable search input in dropdown
errorbooleanfalseError state on trigger
disabledbooleanfalseDisabled
openboolean-Controlled open state
defaultOpenbooleanfalseInitial open state
onValueChange(value: string) => void-Selected value change callback
onOpenChange(open: boolean) => void-Open state change callback
triggerClassNamestring-Custom trigger class
contentClassNamestring-Custom content class
aria-labelstring'select'Accessibility label

SelectOptionEntry

// Basic option
{ value: string; label: ReactNode; disabled?: boolean }

// Group
{ type: 'group'; label: ReactNode; options: SelectOptionItem[] }

// Separator
{ type: 'separator' }

MultiSelect (Multi Chips Mode)

For multi-selection scenarios, use MultiSelect. Selected values are displayed as removable chips in the trigger. Includes an inline input for search filtering and optional creation of new options.

Result
Loading...
Live Editor
render(
  <div style={{ width: 320 }}>
    <MultiSelect
      placeholder="Add email"
      options={[
        { value: 'alice@example.com', label: 'alice@example.com' },
        { value: 'bob@example.com', label: 'bob@example.com' },
        { value: 'charlie@example.com', label: 'charlie@example.com' },
        {
          value: 'a_very_long_email_address@gmail.com',
          label: 'a_very_long_email_address@gmail.com',
        },
      ]}
      defaultValue={['alice@example.com', 'bob@example.com']}
    />
  </div>,
)

MultiSelect States

Result
Loading...
Live Editor
render(
  <div
    style={{
      display: 'grid',
      gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
      gap: 16,
      maxWidth: 760,
    }}
  >
    <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
      <span style={{ fontSize: 12, color: '#999' }}>Normal / Empty</span>
      <MultiSelect
        placeholder="Add email"
        options={[
          { value: 'a', label: 'alice@example.com' },
          { value: 'b', label: 'bob@example.com' },
        ]}
      />
    </div>
    <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
      <span style={{ fontSize: 12, color: '#999' }}>Normal / Filled</span>
      <MultiSelect
        placeholder="Add email"
        options={[
          { value: 'a', label: 'alice@example.com' },
          { value: 'b', label: 'bob@example.com' },
        ]}
        defaultValue={['a', 'b']}
      />
    </div>
    <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
      <span style={{ fontSize: 12, color: '#999' }}>Error</span>
      <MultiSelect
        placeholder="Add email"
        error
        options={[
          { value: 'a', label: 'alice@example.com' },
          { value: 'b', label: 'bob@example.com' },
        ]}
        defaultValue={['a']}
      />
    </div>
    <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
      <span style={{ fontSize: 12, color: '#999' }}>Disabled</span>
      <MultiSelect
        placeholder="Add email"
        disabled
        options={[
          { value: 'a', label: 'alice@example.com' },
          { value: 'b', label: 'bob@example.com' },
          { value: 'c', label: 'my_name_is_too_long_to_read@gmail.com' },
        ]}
        defaultValue={['a', 'b', 'c']}
      />
    </div>
  </div>,
)

Search Filtering

Type in the input to filter options. Backspace on empty input removes the last chip.

Result
Loading...
Live Editor
render(
  <div style={{ width: 320 }}>
    <MultiSelect
      placeholder="Search and select members"
      searchable
      options={[
        { value: 'alice', label: 'Alice Johnson' },
        { value: 'bob', label: 'Bob Smith' },
        { value: 'charlie', label: 'Charlie Brown' },
        { value: 'diana', label: 'Diana Prince' },
        { value: 'eve', label: 'Eve Williams' },
      ]}
    />
  </div>,
)

Creatable

Set searchable + creatable to allow creating new options by typing and pressing Enter.

Result
Loading...
Live Editor
render(
  <div style={{ width: 320 }}>
    <MultiSelect
      placeholder="Add tags (type + Enter)"
      searchable
      creatable
      options={[
        { value: 'bug', label: 'Bug' },
        { value: 'feature', label: 'Feature' },
        { value: 'docs', label: 'Documentation' },
      ]}
      defaultValue={['bug']}
    />
  </div>,
)

MultiSelect Props

PropTypeDefaultDescription
optionsMultiSelectOption[]-Option list (required)
placeholderstring-Placeholder text
valuestring[]-Controlled selected values
defaultValuestring[][]Initial values (uncontrolled)
onChange(value: string[]) => void-Selection change callback
searchablebooleanfalseEnable inline search input
creatablebooleanfalseAllow creating new options via Enter (requires searchable)
onCreate(input: string) => MultiSelectOption-Custom create callback
errorbooleanfalseError state
disabledbooleanfalseDisabled
triggerClassNamestring-Custom trigger class
contentClassNamestring-Custom content class
aria-labelstring'multi-select'Accessibility label

MultiSelectOption

PropTypeDefaultDescription
valuestring-Option value (required)
labelReactNode-Display text
searchTextstring-Custom search text (when label is not a string)
disabledbooleanfalseDisabled