Select
select从下拉浮层里选一个或多个值,超出的已选项折成计数
用法
基础用法
items 提供选项数据,placeholder 作占位。
<Select items={fonts} placeholder="请选择字体">
<SelectTrigger />
<SelectContent>
{fonts.map((f) => (
<SelectItem key={f.value} value={f.value}>
{f.label}
</SelectItem>
))}
</SelectContent>
</Select>默认已选值
非受控写法用 defaultValue 预设选中项。
<Select items={fonts} defaultValue="serif">
<SelectTrigger />
<SelectContent>{/* SelectItem… */}</SelectContent>
</Select>多选
multiple 下受控值为 string[],Trigger 平铺已选 label(超出 maxDisplay 折叠 +N),选中后浮层保持打开。
const [value, setValue] = useState<string[]>([]);
<Select items={fonts} placeholder="选择多个字体" multiple value={value} onValueChange={setValue}>
<SelectTrigger maxDisplay={2} />
<SelectContent>{/* SelectItem… */}</SelectContent>
</Select>chips 多选与单项删除
display=chips 用标签回显已选值;removable 让每项可单独删除,+N 仍提示折叠数量。
const [value, setValue] = useState(["sans", "serif", "mono"]);
<Select items={fonts} multiple value={value} onValueChange={setValue}>
<SelectTrigger display="chips" removable maxDisplay={2} />
<SelectContent>{/* SelectItem… */}</SelectContent>
</Select>搜索后已选项置顶
selectedFirst 只用于多选:先过滤,再将仍命中的已选项按 value 顺序置顶。
<Select items={fonts} multiple searchable selectedFirst defaultValue={["mono", "sans"]}>
<SelectTrigger maxDisplay={2} />
<SelectContent>{/* SelectItem… */}</SelectContent>
</Select>120 项自动虚拟化
searchable 候选达到 100 项时自动虚拟化;selectedFirst 在虚拟窗口计算前完成排序。
<Select items={manyFonts} multiple searchable selectedFirst defaultValue={["font-119"]}>
<SelectTrigger maxDisplay={1} />
<SelectContent>{/* SelectItem… */}</SelectContent>
</Select>可清除
clearable 下有值时 hover / 聚焦字段,右侧箭头位浮出清除按钮;点击置空并回传 null(多选回传 [])。
<Select items={fonts} placeholder="请选择字体" clearable defaultValue="serif">
<SelectTrigger />
<SelectContent>{/* SelectItem… */}</SelectContent>
</Select>可搜索
searchable 切到 Combobox 搜索皮肤:浮层顶部带搜索框,过滤复用 Base UI Combobox(对标 el-select filterable)。
<Select items={fonts} placeholder="请选择字体" searchable clearable>
<SelectTrigger />
<SelectContent>{/* SelectItem… */}</SelectContent>
</Select>加载态
loading 下 Trigger 图标换 Spinner,浮层只出加载占位(不展示上一轮的陈旧选项)。
<Select items={fonts} placeholder="请选择字体" loading loadingText="加载中">
<SelectTrigger />
<SelectContent>{/* SelectItem… */}</SelectContent>
</Select>选项分组
SelectGroup + SelectGroupLabel 给选项分段(Base UI 自动建立 aria 关联)。searchable 皮肤下分组会被拍平。
<SelectContent>
<SelectGroup>
<SelectGroupLabel>西文</SelectGroupLabel>
<SelectItem value="sans">无衬线 Sans</SelectItem>
<SelectItem value="serif">衬线 Serif</SelectItem>
</SelectGroup>
</SelectContent>尺寸
SelectTrigger 的 size 提供 sm / md / lg。
<Select items={fonts} defaultValue="mono">
<SelectTrigger size="sm" />
<SelectContent>{/* SelectItem… */}</SelectContent>
</Select>无效态
SelectTrigger 传 invalid 标红(独立使用时)。
<Select items={fonts} placeholder="请选择字体">
<SelectTrigger invalid />
<SelectContent>{/* SelectItem… */}</SelectContent>
</Select>禁用态
Select 传 disabled 屏蔽整个下拉。
<Select items={fonts} defaultValue="sans" disabled>
<SelectTrigger />
<SelectContent>{/* SelectItem… */}</SelectContent>
</Select>何时用
从一组固定选项里选一项或多项(选项较多、需要收纳成下拉)。多选传 multiple,受控值变 string[],Trigger 平铺已选 label、超出折叠 +N。选项少且需全部可见用 Radio 或 CheckboxGroup(多选平铺);自由文本用 Input。给 items({value,label} 数组)让 Trigger 显示选中项 label 而非 raw value。
对标 el-select 的 clearable / filterable 心智:本组件的 clearable 即前者,searchable 即后者(内部切到 Combobox 的搜索皮肤,过滤逻辑直接复用 Base UI Combobox,不另造)。需要 chips 多选输入、异步远程补全、自由输入等更重的场景仍直接用 Combobox。
导入
import { Select, SelectTrigger, SelectContent, SelectItem, SelectGroup, SelectGroupLabel } from "@hulianui/ui"Props
Select 继承 Base UI Select.Root 属性(除 items 被下方覆盖外,如 value/defaultValue/onValueChange/disabled…)。
Select
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| items | ReadonlyArray<{ value: string | null; label: ReactNode }> | - | 选项数据;Base UI 据此让 Trigger 显示选中项 label |
| defaultValue | string | string[] | null | null | 非受控初值:单选 string | null,multiple 时 string[] |
| placeholder | ReactNode | - | 无选中值时的占位文本(单选注入 value:null 项实现;空 chips 只在真实 Trigger Value 挂载一次,并以稳定关联提供 SSR 可访问名称) |
| multiple | boolean | false | 多选模式:value/defaultValue/onValueChange 均为 string[];选中后浮层保持打开 |
| selectedFirst | boolean | false | 仅多选:按当前 value 数组顺序将已选项置顶,未选项保持原顺序 |
| clearable | boolean | false | 有值时 Trigger 右侧 hover/focus 浮出清除按钮,点击置空(单选回传 null,多选回传 []) |
| searchable | boolean | false | 切到 Combobox 搜索皮肤:浮层顶部搜索框 + Base UI 过滤(依赖 items) |
| searchPlaceholder | string | "搜索" | searchable 时搜索框占位 |
| emptyMessage | ReactNode | "无匹配项" | searchable 时无命中的空态文案 |
| virtualized | boolean | items ≥ 100 时为 true | searchable 皮肤下的列表虚拟化;标准皮肤不涉及。见「禁忌 / 坑」 |
| loading | boolean | false | 加载态:Trigger 图标换 Spinner,浮层只出加载占位(不渲染选项) |
| loadingText | ReactNode | "加载中" | 加载占位文案 |
SelectGroup / SelectGroupLabel
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| children* | ReactNode | - | SelectGroup 内放一个 SelectGroupLabel + 若干 SelectItem |
| className | string | - | 透传类名 |
SelectTrigger
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| size | "xs" | "sm" | "md" | "lg" | "md" | 尺寸。xs 与 Input / Textarea 的 xs 等高(密集表格里同一行三种控件必须对齐) |
| invalid | boolean | false | 独立使用(非 Field 内)时手动置无效态皮肤 |
| maxDisplay | number | 2 | 多选模式下最多平铺几个已选 label,超出折叠为 +N 计数 |
| display | "text" | "chips" | "text" | 多选值的展示方式;chips 用标签视觉回显 |
| removable | boolean | false | display="chips" 时显示单项删除按钮 |
| className | string | - | 透传类名 |
SelectContent
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| side | "top" | "bottom" | "bottom" | 弹出方向 |
| align | "start" | "center" | "end" | - | 对齐 |
| sideOffset | number | - | 偏移量 |
SelectItem
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| value* | string | - | 选项值(本批仅 string 值) |
| disabled | boolean | false | 禁用此项 |
Events
Select 透传 Base UI Select.Root 的常用事件。
| 事件 | 类型 | 说明 |
|---|---|---|
| onValueChange | (value: string | null, eventDetails) => void(多选时 (value: string[], …)) | 选中值变化回调(透传 Base UI Select.Root) |
| onOpenChange | (open: boolean, eventDetails) => void | 下拉开合变化回调(透传 Base UI Select.Root) |
Slots
| 插槽 | 类型 | 说明 |
|---|---|---|
| SelectContent.children* | ReactNode | 一组 SelectItem(可外套 SelectGroup) |
| SelectItem.children* | ReactNode | 选项展示内容 |
| SelectGroupLabel.children* | ReactNode | 分组标题 |
禁忌 / 坑
- 占位文本通过
Select的placeholderprop 传,不要给Select.Value传 placeholder——见 [[base-ui-select-rc0-no-value-placeholder-prop-inject-null-item]]:本项目锁 Base UI rc.0,其Select.Value没有 placeholder prop(那是 v1.2+),瑚琏靠注入一个value:null的 items 项实现占位 label。items与SelectItem的 value 要对应,否则 Trigger 显示 raw value 而非 label。 multiple下 value 必须是数组:给defaultValue="a"(字符串)会被当成无选中处理。多选模式不注入 null 占位项(数组值命不中 null 项),占位由 Trigger 内函数式 Value 渲染,因此多选的 placeholder/label 解析依赖 `items` prop——不传 items 时 Trigger 只能显示 raw value。- 多选 Trigger 的平铺条数由
SelectTrigger的maxDisplay控制(默认 2),不在Select上。 selectedFirst只影响多选。searchable时先过滤、再把仍命中的已选项按 value 数组顺序置顶;未命中的已选值不会被强插回结果。标准皮肤的SelectGroup保持组顺序,只在各组内部重排。display="chips"只改变多选 Trigger 的视觉回显。空数组时,placeholder(支持 ReactNode)只在真实 Trigger Value 中挂载一次:它以 muted 样式直接可见,并带稳定 id;不会再复制到 chips overlay,也不会产生重复 consumer id 或双生命周期。没有消费方显式aria-label/aria-labelledby时,服务端首帧即以aria-labelledby关联这份唯一内容,组件型 placeholder 因而在 SSR 中也能命名控件;显式 aria 属性始终立即优先。子组件在 SSR 时无法检查祖先原生标签,因此带Field或原生<label>的服务端标记可能暂由 placeholder 命名;hydration 检出真实标签后会撤销 fallback,让外部标签恢复优先。已有选中值时,视觉 chip overlay 仍保持aria-hidden,真实 Trigger 使用完整选中 label 命名。removable需要同时开启display="chips",每个删除按钮都是 Trigger 的兄弟节点。clearable仍清空所有值,和单项删除可同时使用。- 值归属:
clearable需要内部 mirror 执行清空;multiple为 chips 单项删除也始终由该 mirror 驱动 Base UI,即使 `clearable=false`。这不改变受控语义:外部传value时仍以外部值为准,外部不回写时清空或单删只回调、不乐观改显示;非受控时 mirror 在未取消的变更后同步更新。 - 清除按钮是
Trigger的兄弟节点(绝对定位盖在箭头位上),不是子节点——<button>里嵌<button>是非法 HTML,且嵌套后点击会冒泡到 Trigger 顺手把浮层打开。常态hidden,靠外层group-hover/group-focus-within浮出。 searchable依赖items:该皮肤下列表由items过滤结果驱动渲染(消费者写的SelectItem按 value 建索引后复用,自定义内容不丢;items有而SelectItem没写的项兜底用 label 渲染)。不传 `items` 就没有候选,浮层恒为空态。searchable下选项会被拍平,SelectGroup不生效(Base UI Combobox 的分组要求items本身是分组结构,与 Select 的声明式分组不是一套)。需要"搜索 + 分组"直接用 Combobox。searchable的过滤匹配 label 的字符串形态;label 传 ReactNode(如带图标的 JSX)时退回按value匹配。要按中文/拼音/编码多字段搜,走 Combobox 自带filter。searchable下items给到 100 项及以上时列表自动虚拟化(底层 Combobox 的策略):只有视口内的选项在 DOM 里,行高按 32px 固定估算、不逐项测量。默认SelectItem恰好 32px,通常无感。如果你的SelectItem高度不是 32px(两行文案、带头像、自定义 padding/字号),那么在 ≥100 项时滚动落位会逐渐偏移——不报错、短列表也复现不出来,请显式传virtualized={false}。同理,测试里getAllByRole("option")在虚拟化后只拿得到视口内那几条。loading期间浮层只出占位、不渲染任何选项(避免展示上一轮的陈旧数据),且不给清除按钮(值可能正在刷新)。loading是展示态,不改值:浮层开着时把选项卸掉,Base UI 会把「已卸载」的选中项当成被移除而回调剔除后的值,本组件在加载期间把这类内部回调吞掉(受控不触发onValueChange,非受控内部值也保留),加载结束后已选项照常显示。注意这只覆盖loading括住的窗口:浮层开着时直接换掉items且新列表不含已选项,Base UI 仍会回调剔除后的值——远程搜索请让已选项留在items里,或改用searchable(Combobox 皮肤没有这条剔除逻辑)。
相关
Input · Textarea · Checkbox · CheckboxGroup · Radio · Switch
Playground
<Select items={items} placeholder="请选择字体" defaultValue="…">
<SelectTrigger size="md" />
<SelectContent side="bottom">
{items.map((it) => <SelectItem key={it.value} value={it.value}>{it.label}</SelectItem>)}
</SelectContent>
</Select>