Select
- 组件说明:单选下拉选择器,可选开启“触发器即输入框”的搜索过滤。对外只提供声明式
options配置 API,触发器无箭头图标,对齐 Figma 规范。 - 尺寸基线:触发器最小高度 40px、水平内边距 16px、圆角 5px。菜单项内边距 16px × 8px。
- 实现约定:
ui/select通过unstyledVisual保留结构骨架,design 层统一接管所有子组件的边框、颜色、阴影和状态视觉。 - Figma 规范
基础用法
声明式 options API — 最简洁的使用方式。设置 searchable 后,触发器会变成可输入的搜索框,并在输入时过滤下拉选项。
render( <div style={{ width: 280 }}> <Select placeholder="请选择成员角色" 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>, )
分组选项
使用 type: 'group' 和 type: 'separator' 实现分类菜单。
render( <div style={{ width: 280 }}> <Select placeholder="请选择团队" 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>, )
状态
触发器状态对齐 Figma 变体矩阵: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="请选择" 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="请选择" 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 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="不可用" 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="不可用" options={[ { value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }, ]} /> </div> </div>, )
Ghost 触发器变体
设置 variant="ghost" 可获得紧凑内联型触发器:无边框、宽度随内容自适应、值右对齐,并显示尾部 ⇅ 上下双向排序箭头。用于行内 / 工具栏 / 弱视觉权重的选择场景;下拉面板与菜单项能力不变。
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>, )
脱离 flex 父容器后,ghost 依然按内容宽度自适应,不会撑满普通块级文档流;hover/focus 灰底也只作用于触发器自身,不会横跨整行宽度。
render( <div> <p>触发器前的一段文字。</p> <Select variant="ghost" defaultValue="week" options={[ { value: 'day', label: 'Day' }, { value: 'week', label: 'Week' }, { value: 'month', label: 'Month' }, ]} /> <p>触发器后的一段文字。</p> </div>, )
选项对勾位置
设置 optionItemVariant="checkmark" 可把选中态对勾从右侧移到左侧(默认 checkmark-right,对勾在右侧)。该能力与 Ghost 触发器变体搭配使用——Ghost 触发器菜单项的 Figma 规范默认演示的正是左置对勾。
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(默认)</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 触发器 + checkmark(左置)</span> <Select variant="ghost" optionItemVariant="checkmark" defaultValue="week" options={[ { value: 'day', label: 'Day' }, { value: 'week', label: 'Week' }, { value: 'month', label: 'Month' }, ]} /> </div> </div>, )
搜索模式
设置 searchable 后,触发器本身会变成输入框,输入时即可实时过滤下拉选项。
render( <div style={{ width: 280 }}> <Select placeholder="搜索并选择" 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>, )
触发器 Token 表
| 状态 | 边框 | 背景 | 说明 |
|---|---|---|---|
| Default | Separators/Emphasized#CCCCCC | 透明 | 未展开 |
| Focused / Open | Labels/Primary#000000 | 透明 | 展开或聚焦 |
| Error | Status/Destructive#FF503F | 透明 | error 属性 |
| Disabled | Separators/Emphasized#CCCCCC | Grays/Gray-1#EBEBEB | disabled 属性 |
Content Token 表
| 元素 | 属性 | Token / 值 |
|---|---|---|
| Content | 背景 | Grays/White#FFFFFF |
| Content | 边框 | Separators/Default#EBEBEB |
| Content | 阴影 | Effects/Shadow/Defaultrgba(0,0,0,0.1) |
| Content | 圆角 | Radius_5 (5px) |
| Content | 最小宽度 | 160px |
| Content | 垂直内边距 | Spacing_8 (8px) |
| Item | 文字颜色 | Labels/Secondary#3D3D3D |
| Item | 悬停背景 | Grays/Gray-1#EBEBEB |
| Item(禁用) | 文字颜色 | Labels/Disabled#A3A3A3 |
| Label | 文字颜色 | Labels/Tertiary#757575 |
| Separator | 颜色 | Separators/Non-opaquergba(0,0,0,0.08) |
尺寸规范
| 维度 | 值 |
|---|---|
| 触发器最小高度 | 40px |
| 触发器水平内边距 | 16px |
| 触发器圆角 | 5px |
| 触发器字体 | Body/Regular(14px,行高 22) |
| 触发器箭头图标 | 无(Figma 规范隐藏) |
| 内容最小宽度 | 160px |
| 内容圆角 | 5px |
| 内容垂直内边距 | 8px |
| 菜单项水平内边距 | 16px |
| 菜单项垂直内边距 | 8px |
| 菜单项字体 | Body/Regular(14px,行高 22) |
| 分割线水平外边距 | 16px |
| 分割线垂直外边距 | 4px |
Props
Select(声明式)
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
options | SelectOptionEntry[] | - | 声明式选项配置 |
placeholder | string | - | 占位文案 |
value | string | - | 受控值 |
defaultValue | string | - | 初始值(非受控) |
variant | 'outline' | 'ghost' | 'outline' | 触发器视觉变体;'ghost' 为紧凑内联型触发器(仅作用于声明式非 searchable 分支) |
optionItemVariant | 'checkmark' | 'checkmark-right' | 'checkmark-right' | 菜单项对勾位置;'checkmark' 移到左侧(仅作用于声明式非 searchable 分支) |
searchable | boolean | false | 启用下拉面板内搜索 |
error | boolean | false | 触发器错误状态 |
disabled | boolean | false | 禁用 |
open | boolean | - | 受控展开状态 |
defaultOpen | boolean | false | 初始展开状态 |
onValueChange | (value: string) => void | - | 选中值变化回调 |
onOpenChange | (open: boolean) => void | - | 展开状态变化回调 |
triggerClassName | string | - | 触发器自定义类名 |
contentClassName | string | - | 内容层自定义类名 |
aria-label | string | 'select' | 无障碍标签 |
SelectOptionEntry
// 基础选项
{ value: string; label: ReactNode; disabled?: boolean }
// 分组
{ type: 'group'; label: ReactNode; options: SelectOptionItem[] }
// 分割线
{ type: 'separator' }
MultiSelect(多选 Chips 模式)
多选场景使用 MultiSelect,已选值在触发器中显示为可移除的 Chip 标签。内置输入框支持搜索过滤,可选开启输入创建新选项。
render( <div style={{ width: 320 }}> <MultiSelect placeholder="添加邮箱" 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 状态
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="添加邮箱" 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="添加邮箱" 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="添加邮箱" 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="添加邮箱" 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>, )
搜索过滤
在输入框中输入即可过滤选项。输入为空时按 Backspace 可删除最后一个 Chip。
render( <div style={{ width: 320 }}> <MultiSelect placeholder="搜索并选择成员" 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>, )
可创建新选项
设置 searchable + creatable 后可通过输入 + 回车创建新选项。
render( <div style={{ width: 320 }}> <MultiSelect placeholder="添加标签(输入 + 回车)" searchable creatable options={[ { value: 'bug', label: 'Bug' }, { value: 'feature', label: 'Feature' }, { value: 'docs', label: 'Documentation' }, ]} defaultValue={['bug']} /> </div>, )
选项风格
下拉选中指示器由两个维度共同决定 —— optionItemVariant 与菜单项是否带前导 icon:
- 形态 ——
checkmark-right/checkmark渲染对勾;checkbox渲染 checkbox(多选场景推荐)。 - 位置 ——
checkmark-right恒在右侧;checkmark/checkbox默认在左侧,当菜单项带前导icon时按 Figma 规范自动移到右侧,让 icon 占据左槽([icon][文字][指示器])。
下面的矩阵把三种 variant 与「有无前导 icon」交叉排列(展开下拉即可看到指示器):
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: '默认 · 对勾在左侧,有 icon 移到右侧' }, { key: 'checkmark-right', note: '对勾恒在右侧' }, { key: 'checkbox', note: 'checkbox 在左侧,有 icon 移到右侧' }, ] const cell = (variant, options) => ( <MultiSelect placeholder="请选择状态" 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('无 icon')} {label('带 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
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
options | MultiSelectOption[] | - | 选项列表(必填) |
placeholder | string | - | 占位文案 |
value | string[] | - | 受控选中值 |
defaultValue | string[] | [] | 初始值(非受控) |
onChange | (value: string[]) => void | - | 选中变化回调 |
searchable | boolean | false | 启用内联搜索输入 |
creatable | boolean | false | 允许输入回车创建新选项(需同时开启 searchable) |
onCreate | (input: string) => MultiSelectOption | - | 自定义创建回调 |
error | boolean | false | 错误状态 |
disabled | boolean | false | 禁用 |
optionItemVariant | 'checkmark' | 'checkmark-right' | 'checkbox' | 'checkmark' | 下拉菜单项风格;'checkmark' = 左侧对勾(默认),多选场景推荐 'checkbox' |
triggerClassName | string | - | 触发器自定义类名 |
contentClassName | string | - | 内容层自定义类名 |
aria-label | string | 'multi-select' | 无障碍标签 |
MultiSelectOption
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
value | string | - | 选项值(必填) |
label | ReactNode | - | 显示文案 |
searchText | string | - | 自定义搜索文本(label 为 ReactNode 时使用) |
disabled | boolean | false | 禁用 |