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>,
)

Ghost Trigger Variant

Set variant="ghost" for a compact inline trigger — no border, content-fit width, right-aligned value, and a trailing sort (up-down) indicator. Used for inline / toolbar / low-emphasis selection scenarios; the dropdown panel and menu items are unchanged.

Result
Loading...
Live Editor
render(
  <div style={{ display: 'flex', gap: 16 }}>
    <Select
      variant="ghost"
      defaultValue="week"
      options={[
        { value: 'day', label: 'Day' },
        { value: 'week', label: 'Week' },
        { value: 'month', label: 'Month' },
      ]}
    />
    <Select
      variant="ghost"
      disabled
      defaultValue="week"
      options={[
        { value: 'day', label: 'Day' },
        { value: 'week', label: 'Week' },
        { value: 'month', label: 'Month' },
      ]}
    />
  </div>,
)

Outside a flex parent, ghost still shrinks to its content width instead of stretching to fill the block-level flow — the hover/focus gray background stays scoped to the trigger itself and never spans the full line width.

Result
Loading...
Live Editor
render(
  <div>
    <p>Some text before the trigger.</p>
    <Select
      variant="ghost"
      defaultValue="week"
      options={[
        { value: 'day', label: 'Day' },
        { value: 'week', label: 'Week' },
        { value: 'month', label: 'Month' },
      ]}
    />
    <p>Some text after the trigger.</p>
  </div>,
)

Option Item Variant (Checkmark Placement)

Set optionItemVariant="checkmark" to move the selected-state check to the left of the label (default is checkmark-right, check on the right). This pairs with the Ghost trigger variant, whose Figma spec defaults to a left-aligned checkmark.

Result
Loading...
Live Editor
render(
  <div style={{ display: 'flex', gap: 32 }}>
    <div style={{ display: 'flex', flexDirection: 'column', gap: 4, width: 200 }}>
      <span style={{ fontSize: 12, color: '#999' }}>checkmark-right (default)</span>
      <Select
        defaultValue="week"
        options={[
          { value: 'day', label: 'Day' },
          { value: 'week', label: 'Week' },
          { value: 'month', label: 'Month' },
        ]}
      />
    </div>
    <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
      <span style={{ fontSize: 12, color: '#999' }}>ghost trigger + checkmark (left)</span>
      <Select
        variant="ghost"
        optionItemVariant="checkmark"
        defaultValue="week"
        options={[
          { value: 'day', label: 'Day' },
          { value: 'week', label: 'Week' },
          { value: 'month', label: 'Month' },
        ]}
      />
    </div>
  </div>,
)

Options with Icon

Pass icon on each option to show a leading icon in both the dropdown and the trigger fill-back display.

Result
Loading...
Live Editor
render(
  <div style={{ width: 280 }}>
    <Select
      placeholder="Select status"
      options={[
        { value: 'active', label: 'Active', icon: <span style={{ width: 8, height: 8, borderRadius: '50%', background: '#22c55e', display: 'inline-block' }} /> },
        { value: 'pending', label: 'Pending', icon: <span style={{ width: 8, height: 8, borderRadius: '50%', background: '#f59e0b', display: 'inline-block' }} /> },
        { value: 'inactive', label: 'Inactive', icon: <span style={{ width: 8, height: 8, borderRadius: '50%', background: '#a3a3a3', display: 'inline-block' }} /> },
      ]}
    />
  </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

Content Token Table

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)
variant'outline' | 'ghost''outline'Trigger visual variant; 'ghost' is a compact inline trigger (applies to the declarative non-searchable branch only)
optionItemVariant'checkmark' | 'checkmark-right''checkmark-right'Menu item checkmark placement; 'checkmark' moves it to the left (applies to the declarative non-searchable branch only)
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; icon?: 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>,
)

Option Item Variants

The dropdown selection indicator is decided by two dimensions together — the optionItemVariant and whether the item has a leading icon:

  • Formcheckmark-right / checkmark render a check; checkbox renders a checkbox (recommended for multi-select).
  • Positioncheckmark-right is always on the right. checkmark / checkbox sit on the left, and automatically move to the right when the item has a leading icon, so the icon can occupy the left slot ([icon][text][indicator]), per the Figma spec.

The matrix below crosses all three variants with / without a leading icon (open a dropdown to see the indicator):

Result
Loading...
Live Editor
const dot = (color) => (
  <span style={{ width: 8, height: 8, borderRadius: '50%', background: color, display: 'inline-block' }} />
)

const PLAIN_OPTIONS = [
  { value: 'active', label: 'Active' },
  { value: 'pending', label: 'Pending' },
  { value: 'inactive', label: 'Inactive' },
]

const ICON_OPTIONS = [
  { value: 'active', label: 'Active', icon: dot('#22c55e') },
  { value: 'pending', label: 'Pending', icon: dot('#f59e0b') },
  { value: 'inactive', label: 'Inactive', icon: dot('#a3a3a3') },
]

const VARIANTS = [
  { key: 'checkmark', note: 'default · check on the left, moves right with icon' },
  { key: 'checkmark-right', note: 'check always on the right' },
  { key: 'checkbox', note: 'checkbox on the left, moves right with icon' },
]

const cell = (variant, options) => (
  <MultiSelect
    placeholder="Select status"
    optionItemVariant={variant}
    options={options}
    defaultValue={['active']}
  />
)

const label = (text) => <span style={{ fontSize: 12, color: '#999' }}>{text}</span>

render(
  <div
    style={{
      display: 'grid',
      gridTemplateColumns: '180px 1fr 1fr',
      gap: 12,
      alignItems: 'center',
      maxWidth: 680,
    }}
  >
    <span />
    {label('No icon')}
    {label('With icon')}
    {VARIANTS.flatMap((v) => [
      <div key={`${v.key}-title`} style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
        <code style={{ fontSize: 12 }}>{v.key}</code>
        {label(v.note)}
      </div>,
      <div key={`${v.key}-plain`}>{cell(v.key, PLAIN_OPTIONS)}</div>,
      <div key={`${v.key}-icon`}>{cell(v.key, ICON_OPTIONS)}</div>,
    ])}
  </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
optionItemVariant'checkmark' | 'checkmark-right' | 'checkbox''checkmark'Dropdown item style; 'checkmark' = left checkmark (default), 'checkbox' recommended for multi-select
triggerClassNamestring-Custom trigger class
contentClassNamestring-Custom content class
aria-labelstring'multi-select'Accessibility label

MultiSelectOption

PropTypeDefaultDescription
valuestring-Option value (required)
labelReactNode-Display text
iconReactNode-Leading icon rendered before the label
searchTextstring-Custom search text (when label is not a string)
disabledbooleanfalseDisabled