Select
- Component overview: Single-selection dropdown with optional trigger-input search filtering. The public API uses a declarative
optionsconfiguration, 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/selectretains structural skeleton withunstyledVisual; 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.
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.
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.
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.
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.
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.
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.
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.
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
| State | Border | Background | Note |
|---|---|---|---|
| Default | Separators/Emphasized#CCCCCC | transparent | Collapsed |
| Focused / Open | Labels/Primary#000000 | transparent | Expanded or focused |
| Error | Status/Destructive#FF503F | transparent | error prop |
| Disabled | Separators/Emphasized#CCCCCC | Grays/Gray-1#EBEBEB | disabled prop |
Content Token Table
| Element | Property | Token / Value |
|---|---|---|
| Content | Background | Grays/White#FFFFFF |
| Content | Border | Separators/Default#EBEBEB |
| Content | Shadow | Effects/Shadow/Defaultrgba(0,0,0,0.1) |
| Content | Border radius | Radius_5 (5px) |
| Content | Min width | 160px |
| Content | Padding (vertical) | Spacing_8 (8px) |
| Item | Text color | Labels/Secondary#3D3D3D |
| Item | Hover background | Grays/Gray-1#EBEBEB |
| Item (disabled) | Text color | Labels/Disabled#A3A3A3 |
| Label | Text color | Labels/Tertiary#757575 |
| Separator | Color | Separators/Non-opaquergba(0,0,0,0.08) |
Size Spec
| Dimension | Value |
|---|---|
| Trigger min-height | 40px |
| Trigger horizontal padding | 16px |
| Trigger border radius | 5px |
| Trigger font | Body/Regular (14px, line-height 22) |
| Trigger arrow icon | None (hidden per Figma) |
| Content min-width | 160px |
| Content border radius | 5px |
| Content vertical padding | 8px |
| Item horizontal padding | 16px |
| Item vertical padding | 8px |
| Item font | Body/Regular (14px, line-height 22) |
| Separator horizontal margin | 16px |
| Separator vertical margin | 4px |
Props
Select (declarative)
| Prop | Type | Default | Description |
|---|---|---|---|
options | SelectOptionEntry[] | - | Declarative option entries |
placeholder | string | - | Placeholder text |
value | string | - | Controlled value |
defaultValue | string | - | 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) |
searchable | boolean | false | Enable search input in dropdown |
error | boolean | false | Error state on trigger |
disabled | boolean | false | Disabled |
open | boolean | - | Controlled open state |
defaultOpen | boolean | false | Initial open state |
onValueChange | (value: string) => void | - | Selected value change callback |
onOpenChange | (open: boolean) => void | - | Open state change callback |
triggerClassName | string | - | Custom trigger class |
contentClassName | string | - | Custom content class |
aria-label | string | '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.
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
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.
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.
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:
- Form —
checkmark-right/checkmarkrender a check;checkboxrenders a checkbox (recommended for multi-select). - Position —
checkmark-rightis always on the right.checkmark/checkboxsit on the left, and automatically move to the right when the item has a leadingicon, 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):
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
| Prop | Type | Default | Description |
|---|---|---|---|
options | MultiSelectOption[] | - | Option list (required) |
placeholder | string | - | Placeholder text |
value | string[] | - | Controlled selected values |
defaultValue | string[] | [] | Initial values (uncontrolled) |
onChange | (value: string[]) => void | - | Selection change callback |
searchable | boolean | false | Enable inline search input |
creatable | boolean | false | Allow creating new options via Enter (requires searchable) |
onCreate | (input: string) => MultiSelectOption | - | Custom create callback |
error | boolean | false | Error state |
disabled | boolean | false | Disabled |
optionItemVariant | 'checkmark' | 'checkmark-right' | 'checkbox' | 'checkmark' | Dropdown item style; 'checkmark' = left checkmark (default), 'checkbox' recommended for multi-select |
triggerClassName | string | - | Custom trigger class |
contentClassName | string | - | Custom content class |
aria-label | string | 'multi-select' | Accessibility label |
MultiSelectOption
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | Option value (required) |
label | ReactNode | - | Display text |
icon | ReactNode | - | Leading icon rendered before the label |
searchText | string | - | Custom search text (when label is not a string) |
disabled | boolean | false | Disabled |