Combobox
combobox边输入边过滤候选项,可用触发按钮也可内联输入
用法
弹层内搜索(图4 范式)
触发按钮显示已选项,点击展开带搜索框的浮层。
<Combobox items={fruits}>
<ComboboxTrigger placeholder="选择水果" />
<ComboboxContent searchPlaceholder="搜索水果…">
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxContent>
</Combobox>内联自动补全
输入框本身即可见字段,直接打字过滤,clearable 显示清除钮。
<Combobox items={fruits}>
<ComboboxInput placeholder="搜索水果…" clearable />
<ComboboxContent>
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxContent>
</Combobox>搜索框形态
prefix 放大镜 + showChevron={false}:字段本身就是搜索框(输入过滤 + 选中跳转),不该长得像下拉选择。独立使用时 aria-label 直接传给 ComboboxInput 即可,它落到内层 input。
<Combobox items={fruits}>
<ComboboxInput
size="sm"
prefix={<SearchIcon />}
showChevron={false}
placeholder="搜索水果"
aria-label="搜索水果"
/>
<ComboboxContent>
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxContent>
</Combobox>默认已选值
defaultValue 传入选项对象作为非受控初值。
<Combobox items={fruits} defaultValue={fruits[2]}>
<ComboboxTrigger placeholder="选择水果" />
<ComboboxContent searchPlaceholder="搜索水果…">
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxContent>
</Combobox>触发钮退化成状态图标
children 整段替换掉默认的「已选 label」。表格窄格子里名字已经在别处出现过一次,触发钮再显示一遍就像两个字段——那种位置只放得下一枚图标。传函数即按有没有选中分叉。
<Combobox items={fruits}>
<ComboboxTrigger
aria-label="绑定水果"
showChevron={false}
className="size-8 justify-center px-0"
>
{(value) => (value ? <Link className="size-4" /> : <Plus className="size-4" />)}
</ComboboxTrigger>
<ComboboxContent searchPlaceholder="搜索水果…">
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxContent>
</Combobox>禁用与无效态
disabled 整体置灰;invalid 触发器标红。
<>
<Combobox items={fruits} defaultValue={fruits[0]} disabled>
<ComboboxTrigger placeholder="选择水果" />
<ComboboxContent searchPlaceholder="搜索水果…">
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxContent>
</Combobox>
<Combobox items={fruits}>
<ComboboxTrigger placeholder="选择水果" invalid />
<ComboboxContent searchPlaceholder="搜索水果…">
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxContent>
</Combobox>
</>尺寸
size 控制触发器高度(sm / md / lg)。
<Combobox items={fruits}>
<ComboboxTrigger size="sm" placeholder="选择水果" />
<ComboboxContent searchPlaceholder="搜索水果…">
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxContent>
</Combobox>何时用
选项较多需边打字边过滤(自动补全/可搜索下拉)时用;支持单选、multiple 多选 chips、触发按钮式弹层内搜索,以及内联输入直接过滤三种范式。选项少且全可见用 Select;纯展示已有密钥用 SecretField;@提及场景用 Mentions。
导入
import { Combobox, ComboboxInput, ComboboxTrigger, ComboboxContent, ComboboxItem, ComboboxChips, ComboboxChip } from "@hulianui/ui"Props
Combobox(透传 Base UI Combobox.Root<ComboboxItemData, Multiple>,下表为瑚琏常用项)
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| items | ComboboxItemData[] | - | 选项数据 {value,label},自动以 label 显示、value 提交 |
| value | ComboboxItemData|ComboboxItemData[] | - | 受控选中(multiple 时为数组) |
| defaultValue | 同上 | - | 非受控初始选中 |
| multiple | boolean | false | true 时 value/onValueChange 自动变数组 |
| virtualized | boolean | items 长度 ≥ 100 时为 true | 列表虚拟化(只渲染视口内的项)。不传时按选项数自动决定,见「禁忌 / 坑」 |
| creatable | boolean | false | 自由输入创建新值:当前输入串在候选里没有完全相同的一项时,列表首位多出一条「使用 “xxx”」。见下 |
| onCreate | (value: string) => void | - | 创建项被选中时触发(与 onValueChange 同时发生,不是二选一)。见下 |
| createLabel | (value: string) => ReactNode | 全局 locale 的 combobox.create | 创建项那一行的文案,单点覆盖。同一个应用里两个 creatable 说的常常不是同一件事 |
| disabled | boolean | false | 禁用 |
ComboboxTrigger(图4 范式:显示已选 label / placeholder,点击展开弹层内搜索)。继承原生 <button> 属性,剩余属性落到按钮自身。
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| size | "sm"|"md"|"lg" | "md" | 尺寸 |
| placeholder | string | - | 未选中时占位文案(按钮没有原生 placeholder,这是瑚琏语义) |
| invalid | boolean | false | 独立使用(非 Field 内)时手动置无效态皮肤 |
| showChevron | boolean | true | 右侧展开箭头。退化成图标钮时传 false |
| className | string | - | - |
ComboboxInput(内联自动补全:输入框本身即可见字段)。继承原生 <input> 属性,剩余属性落到内层 `<input>`(不是外壳 span),见「禁忌 / 坑」。
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| size | "sm"|"md"|"lg" | "md" | 尺寸 |
| placeholder | string | - | 占位(原生属性,透传到内层 input) |
| invalid | boolean | false | 手动置无效态皮肤 |
| clearable | boolean | false | 有值时渲染清除按钮 |
| prefix | ReactNode | - | 字段左侧图标槽(对齐 Input.prefix),搜索框放放大镜 |
| showChevron | boolean | true | 右侧展开箭头;搜索框形态传 false |
| className | string | - | 外壳类名(皮肤在外壳上,不随 rest 落到 input) |
ComboboxContent
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| searchPlaceholder | string | - | 设置后在浮层顶部渲染搜索框(图4 范式,配合 Trigger);不设则为内联补全态 |
| side | "top"|"bottom" | - | 浮层方位 |
| align | "start"|"center"|"end" | - | 浮层对齐 |
| sideOffset | number | - | 偏移 |
| onListScroll | UIEventHandler<HTMLDivElement> | - | 列表滚动回调,e.currentTarget 即滚动容器(远程分页「滚到底加载更多」用,见 RemoteSelect) |
| header | ReactNode | - | 列表上方常驻表头(用法提示、分组说明、批量操作),不随列表滚动。与 emptyMessage 不同:后者只在零结果时出现 |
| footer | ReactNode | - | 列表下方常驻页脚(加载中 / 计数 / 到底提示),不随列表滚动 |
| className | string | - | - |
ComboboxItem
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| value * | ComboboxItemData | - | 选项 {value,label} 对象 |
| disabled | boolean | false | 禁用该项 |
| className | string | - | - |
ComboboxChips(多选 chips 外壳):size、invalid、placeholder、className(外加 children 插槽,见 Slots)。继承原生 <input> 属性,剩余属性落到内层 `<input>`(chips 容器只是皮肤壳)。ComboboxChip(单个已选 chip):className(外加 children 插槽,见 Slots)。
Events
Combobox(透传 Base UI Combobox.Root)
| 事件 | 类型 | 说明 |
|---|---|---|
| onValueChange | (value) => void | 选中变化回调(multiple 时 value 为数组) |
Slots
Combobox
| 插槽 | 类型 | 说明 |
|---|---|---|
| children | ReactNode | 内放 Trigger/Input + Content |
ComboboxTrigger
| 插槽 | 类型 | 说明 |
|---|---|---|
| children | ReactNode | (value: ComboboxItemData | null) => ReactNode | 自定义触发钮内容,替换掉默认那块「已选 label ?? placeholder」。传函数即按已选值分叉。不传 = 原行为 |
ComboboxContent
| 插槽 | 类型 | 说明 |
|---|---|---|
| children * | (item, index) => ReactNode | 渲染函数,List 自动遍历已过滤项调用 |
| emptyMessage | ReactNode | 无匹配时文案 |
ComboboxItem
| 插槽 | 类型 | 说明 |
|---|---|---|
| children * | ReactNode | 渲染内容 |
ComboboxChips
| 插槽 | 类型 | 说明 |
|---|---|---|
| children | ReactNode | 内含 chip 列 + 输入框 |
ComboboxChip
| 插槽 | 类型 | 说明 |
|---|---|---|
| children * | ReactNode | chip 内容 |
禁忌 / 坑
ComboboxItem的value是整个{value,label}对象(非字符串)——render fn 里直接传value={item},Base UI 自动派生 label/value。- 浮层内搜索框由
ComboboxContent的searchPlaceholder触发:配ComboboxTrigger用就给它(图4 范式),配ComboboxInput内联补全则不设(输入框本身即搜索)。 multiple一开 value/onValueChange 即变数组,受控时 state 类型要跟着变。invalid仅用于「非 Field 内」独立使用时手动置无效皮肤;在 Field 内由 Field 接管,不用手传。- `ComboboxInput` / `ComboboxChips` 的剩余原生属性落在内层 `<input>`,不是外壳:
role="combobox"、可聚焦性、表单归属都在内层,aria-label/id/name/onBlur挂在外壳<span>/ chips 容器上一律无效。所以独立使用(不放在 Field 里)时,直接<ComboboxInput aria-label="搜索任务" />就够了,不用再包一层<label>+.sr-only;接 react-hook-form 的Controller时field.onBlur也是直接传。要给外层容器加钩子请用className。ComboboxTrigger没有外壳,剩余属性就落在按钮自身。 - 组件自身的
data-invalid/ 皮肤类名顶不掉:rest展开在最前(同docs/consuming.md第 7 节的全库口径),传aria-invalid={false}不会关掉invalid的无效态。 - 搜索框形态要同时给
prefix和showChevron={false}:只加放大镜、右边还留着 chevron 的字段读起来仍是「下拉选择」。反过来,弹层内搜索(图4 范式)的搜索框由ComboboxContent的searchPlaceholder提供,自带放大镜,不需要动ComboboxInput。 - `items` 给到 100 项及以上时列表会自动虚拟化(无需传
virtualized):只有视口内的项在 DOM 里,行高按 32px 固定估算,不做逐项测量。默认ComboboxItem恰好是 32px,所以通常无感。如果你的 render fn 返回的项高度不是 32px(两行文案、带头像/副标题、自定义className改了 padding 或字号),那么在 ≥100 项时滚动条长度与项的落位会逐渐偏移——页面不会报任何错,短列表下也复现不出来,只有滚到列表中后段才看得出跳动。这种项请显式传virtualized={false}关掉,或把项高度对齐到 32px。 - 虚拟化同样影响依赖「选项全在 DOM 里」的测试与脚本:
getAllByRole("option")只会拿到视口内那几条,document.querySelector找不到未滚动到的项。断言总数请改用列表容器上的data-hulian-virtual-count,或对该用例传virtualized={false}。 - 走 Combobox 的上层组件同样吃这条:Select 的
searchable皮肤、RemoteSelect 的候选列表,选项攒到 100 条后都会自动虚拟化。
相关
SecretField · Listbox · Mentions · InputOTP · Rating · Upload
Playground
<Combobox items={items} defaultValue={items[0]}>
<ComboboxTrigger size="md" placeholder="选择水果" />
<ComboboxContent searchPlaceholder="搜索水果…">
{(item) => <ComboboxItem key={item.value} value={item}>{item.label}</ComboboxItem>}
</ComboboxContent>
</Combobox>