跳到主要内容

Select

  • 组件说明:单选下拉选择器,可选开启“触发器即输入框”的搜索过滤。对外只提供声明式 options 配置 API,触发器无箭头图标,对齐 Figma 规范。
  • 尺寸基线:触发器最小高度 40px、水平内边距 16px、圆角 5px。菜单项内边距 16px × 8px。
  • 实现约定ui/select 通过 unstyledVisual 保留结构骨架,design 层统一接管所有子组件的边框、颜色、阴影和状态视觉。
  • Figma 规范

基础用法

声明式 options API — 最简洁的使用方式。设置 searchable 后,触发器会变成可输入的搜索框,并在输入时过滤下拉选项。

结果
Loading...
实时编辑器
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' 实现分类菜单。

结果
Loading...
实时编辑器
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。

结果
Loading...
实时编辑器
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" 可获得紧凑内联型触发器:无边框、宽度随内容自适应、值右对齐,并显示尾部 ⇅ 上下双向排序箭头。用于行内 / 工具栏 / 弱视觉权重的选择场景;下拉面板与菜单项能力不变。

结果
Loading...
实时编辑器
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 灰底也只作用于触发器自身,不会横跨整行宽度。

结果
Loading...
实时编辑器
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 规范默认演示的正是左置对勾。

结果
Loading...
实时编辑器
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 后,触发器本身会变成输入框,输入时即可实时过滤下拉选项。

结果
Loading...
实时编辑器
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 表

状态边框背景说明
DefaultSeparators/Emphasized#CCCCCC透明未展开
Focused / OpenLabels/Primary#000000透明展开或聚焦
ErrorStatus/Destructive#FF503F透明error 属性
DisabledSeparators/Emphasized#CCCCCCGrays/Gray-1#EBEBEBdisabled 属性

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(声明式)

属性类型默认值说明
optionsSelectOptionEntry[]-声明式选项配置
placeholderstring-占位文案
valuestring-受控值
defaultValuestring-初始值(非受控)
variant'outline' | 'ghost''outline'触发器视觉变体;'ghost' 为紧凑内联型触发器(仅作用于声明式非 searchable 分支)
optionItemVariant'checkmark' | 'checkmark-right''checkmark-right'菜单项对勾位置;'checkmark' 移到左侧(仅作用于声明式非 searchable 分支)
searchablebooleanfalse启用下拉面板内搜索
errorbooleanfalse触发器错误状态
disabledbooleanfalse禁用
openboolean-受控展开状态
defaultOpenbooleanfalse初始展开状态
onValueChange(value: string) => void-选中值变化回调
onOpenChange(open: boolean) => void-展开状态变化回调
triggerClassNamestring-触发器自定义类名
contentClassNamestring-内容层自定义类名
aria-labelstring'select'无障碍标签

SelectOptionEntry

// 基础选项
{ value: string; label: ReactNode; disabled?: boolean }

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

// 分割线
{ type: 'separator' }

MultiSelect(多选 Chips 模式)

多选场景使用 MultiSelect,已选值在触发器中显示为可移除的 Chip 标签。内置输入框支持搜索过滤,可选开启输入创建新选项。

结果
Loading...
实时编辑器
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 状态

结果
Loading...
实时编辑器
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。

结果
Loading...
实时编辑器
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 后可通过输入 + 回车创建新选项。

结果
Loading...
实时编辑器
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」交叉排列(展开下拉即可看到指示器):

结果
Loading...
实时编辑器
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

属性类型默认值说明
optionsMultiSelectOption[]-选项列表(必填)
placeholderstring-占位文案
valuestring[]-受控选中值
defaultValuestring[][]初始值(非受控)
onChange(value: string[]) => void-选中变化回调
searchablebooleanfalse启用内联搜索输入
creatablebooleanfalse允许输入回车创建新选项(需同时开启 searchable
onCreate(input: string) => MultiSelectOption-自定义创建回调
errorbooleanfalse错误状态
disabledbooleanfalse禁用
optionItemVariant'checkmark' | 'checkmark-right' | 'checkbox''checkmark'下拉菜单项风格;'checkmark' = 左侧对勾(默认),多选场景推荐 'checkbox'
triggerClassNamestring-触发器自定义类名
contentClassNamestring-内容层自定义类名
aria-labelstring'multi-select'无障碍标签

MultiSelectOption

属性类型默认值说明
valuestring-选项值(必填)
labelReactNode-显示文案
searchTextstring-自定义搜索文本(label 为 ReactNode 时使用)
disabledbooleanfalse禁用