表单组件
组合框
可搜索的下拉选择框,基于 @base-ui/react/combobox 构建,支持单选、多选(Chips 模式)、自定义过滤,适用于大量选项的选择场景。
安装
bash
pnpm dlx shadcn@latest add combobox -c packages/react基础
tsx
import { useState } from "react"
import { Combobox, ComboboxInput, ComboboxContent, ComboboxList, ComboboxItem, ComboboxEmpty, ComboboxGroup } from "@polyui/react/combobox"
export function ComboboxBasic() {
const [value, setValue] = useState<string | null>(null)
return (
<Combobox value={value} onValueChange={setValue}>
<ComboboxInput placeholder="Select framework..." showTrigger className="w-64" />
<ComboboxContent>
<ComboboxList>
<ComboboxEmpty>No framework found.</ComboboxEmpty>
<ComboboxGroup>
{frameworks.map((fw) => (
<ComboboxItem key={fw.value} value={fw.value}>
{fw.label}
</ComboboxItem>
))}
</ComboboxGroup>
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}分组
tsx
import { Fragment, useState } from "react"
import { Combobox, ComboboxInput, ComboboxContent, ComboboxList, ComboboxItem, ComboboxEmpty, ComboboxGroup, ComboboxLabel } from "@polyui/react/combobox"
export function ComboboxGrouped() {
const [value, setValue] = useState<string | null>(null)
return (
<Combobox value={value} onValueChange={setValue}>
<ComboboxInput placeholder="Select item..." showTrigger className="w-64" />
<ComboboxContent>
<ComboboxList>
<ComboboxEmpty>No item found.</ComboboxEmpty>
{groupedItems.map((g) => (
<Fragment key={g.group}>
<ComboboxGroup>
<ComboboxLabel>{g.group}</ComboboxLabel>
{g.items.map((item) => (
<ComboboxItem key={item.value} value={item.value}>
{item.value}
</ComboboxItem>
))}
</ComboboxGroup>
</Fragment>
))}
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}禁用选项
tsx
import { Fragment, useState } from "react"
import { Combobox, ComboboxInput, ComboboxContent, ComboboxList, ComboboxItem, ComboboxEmpty, ComboboxGroup, ComboboxLabel } from "@polyui/react/combobox"
export function ComboboxDisabledOptions() {
const [value, setValue] = useState<string | null>(null)
return (
<Combobox value={value} onValueChange={setValue}>
<ComboboxInput placeholder="Select item..." showTrigger className="w-64" />
<ComboboxContent>
<ComboboxList>
<ComboboxEmpty>No item found.</ComboboxEmpty>
{groupedWithDisabled.map((g) => (
<Fragment key={g.category}>
<ComboboxGroup>
<ComboboxLabel>{g.category}</ComboboxLabel>
{g.items.map((item) => (
<ComboboxItem key={item.value} value={item.value} disabled={item.disabled}>
{item.value}
</ComboboxItem>
))}
</ComboboxGroup>
</Fragment>
))}
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}带图标
tsx
import { useState } from "react"
import { Combobox, ComboboxInput, ComboboxContent, ComboboxList, ComboboxItem, ComboboxEmpty, ComboboxGroup } from "@polyui/react/combobox"
export function ComboboxWithIcons() {
const [value, setValue] = useState<string | null>(null)
return (
<Combobox value={value} onValueChange={setValue}>
<ComboboxInput placeholder="Select industry..." showTrigger className="w-64" />
<ComboboxContent>
<ComboboxList>
<ComboboxEmpty>No industry found.</ComboboxEmpty>
<ComboboxGroup>
{industries.map((industry) => (
<ComboboxItem key={industry.value} value={industry.value}>
<industry.icon className="size-4 text-muted-foreground" />
{industry.label}
</ComboboxItem>
))}
</ComboboxGroup>
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}自定义选中图标
Custom check icon (blue circle)
tsx
import { useState } from "react"
import { Combobox, ComboboxInput, ComboboxContent, ComboboxList, ComboboxItem, ComboboxEmpty, ComboboxGroup } from "@polyui/react/combobox"
export function ComboboxCustomCheckIcon() {
const [value, setValue] = useState<string | null>(null)
return (
<div className="space-y-2">
<p className="text-sm text-muted-foreground">Custom check icon (blue circle)</p>
<Combobox value={value} onValueChange={setValue}>
<ComboboxInput placeholder="Select framework..." showTrigger className="w-64" />
<ComboboxContent>
<ComboboxList>
<ComboboxEmpty>No framework found.</ComboboxEmpty>
<ComboboxGroup>
{frameworks.map((fw) => (
<ComboboxItem key={fw.value} value={fw.value}>
{fw.label}
</ComboboxItem>
))}
</ComboboxGroup>
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
)
}带添加按钮
tsx
import { useState } from "react"
import { Combobox, ComboboxInput, ComboboxContent, ComboboxList, ComboboxItem, ComboboxEmpty, ComboboxGroup, ComboboxSeparator } from "@polyui/react/combobox"
import { Button } from "@polyui/react/button"
import { PlusIcon } from "lucide-react"
export function ComboboxWithAddButton() {
const [value, setValue] = useState<string | null>("harvard")
return (
<Combobox value={value} onValueChange={setValue}>
<ComboboxInput placeholder="Find university..." showTrigger className="w-64" />
<ComboboxContent>
<ComboboxList>
<ComboboxEmpty>No university found.</ComboboxEmpty>
<ComboboxGroup>
{universities.map((u) => (
<ComboboxItem key={u.value} value={u.value}>
{u.label}
</ComboboxItem>
))}
</ComboboxGroup>
<ComboboxSeparator />
<div className="p-1">
<Button variant="ghost" className="w-full justify-start font-normal text-sm">
<PlusIcon className="opacity-60" />
New university
</Button>
</div>
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}时区
tsx
import { useMemo, useState } from "react"
import { Combobox, ComboboxInput, ComboboxContent, ComboboxList, ComboboxItem, ComboboxEmpty, ComboboxGroup } from "@polyui/react/combobox"
export function ComboboxTimezone() {
const [value, setValue] = useState<string | null>("Indian/Cocos")
const timezones = useMemo(() => {
return Intl.supportedValuesOf("timeZone")
.map((tz) => {
const formatter = new Intl.DateTimeFormat("en", { timeZone: tz, timeZoneName: "shortOffset" })
const parts = formatter.formatToParts(new Date())
const offset = parts.find((p) => p.type === "timeZoneName")?.value || ""
const formattedOffset = offset === "GMT" ? "GMT+0" : offset
return {
value: tz,
label: `(${formattedOffset}) ${tz.replace(/_/g, " ")}`,
numericOffset: parseInt(formattedOffset.replace("GMT", "").replace("+", "") || "0"),
}
})
.sort((a, b) => a.numericOffset - b.numericOffset)
}, [])
return (
<Combobox value={value} onValueChange={setValue}>
<ComboboxInput placeholder="Select timezone..." showTrigger className="w-72" />
<ComboboxContent>
<ComboboxList>
<ComboboxEmpty>No timezone found.</ComboboxEmpty>
<ComboboxGroup>
{timezones.map(({ value: v, label }) => (
<ComboboxItem key={v} value={v}>
<span className="truncate">{label}</span>
</ComboboxItem>
))}
</ComboboxGroup>
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}用户
tsx
import { useState } from "react"
import { Combobox, ComboboxInput, ComboboxContent, ComboboxList, ComboboxItem, ComboboxEmpty, ComboboxGroup } from "@polyui/react/combobox"
import { Avatar, AvatarFallback, AvatarImage } from "@polyui/react/avatar"
export function ComboboxUsers() {
const [value, setValue] = useState<string | null>(null)
return (
<Combobox value={value} onValueChange={setValue}>
<ComboboxInput placeholder="Select user..." showTrigger className="w-72" />
<ComboboxContent>
<ComboboxList>
<ComboboxEmpty>No users found.</ComboboxEmpty>
<ComboboxGroup>
{users.map((user) => (
<ComboboxItem key={user.name} value={user.name}>
<Avatar className="size-6">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback>{user.name[0]}</AvatarFallback>
</Avatar>
<span className="flex flex-col">
<span className="text-sm font-medium">{user.name}</span>
<span className="text-xs text-muted-foreground">{user.email}</span>
</span>
</ComboboxItem>
))}
</ComboboxGroup>
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}国家旗帜
tsx
import { useState } from "react"
import { Combobox, ComboboxInput, ComboboxContent, ComboboxList, ComboboxItem, ComboboxEmpty, ComboboxGroup } from "@polyui/react/combobox"
export function ComboboxCountryFlag() {
const [value, setValue] = useState<string | null>(null)
return (
<Combobox value={value} onValueChange={setValue}>
<ComboboxInput placeholder="Select country..." showTrigger className="w-64" />
<ComboboxContent>
<ComboboxList>
<ComboboxEmpty>No country found.</ComboboxEmpty>
<ComboboxGroup>
{countries.map((country) => (
<ComboboxItem key={country.value} value={country.value}>
<img src={country.flag} alt={country.value} className="h-4 w-5 object-cover" />
{country.value}
</ComboboxItem>
))}
</ComboboxGroup>
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}多选
React
tsx
import { useState } from "react"
import { Combobox, ComboboxContent, ComboboxList, ComboboxItem, ComboboxEmpty, ComboboxGroup, ComboboxChips, ComboboxChip, ComboboxChipsInput } from "@polyui/react/combobox"
export function ComboboxMultiple() {
const [values, setValues] = useState<string[]>(["react"])
return (
<Combobox multiple value={values} onValueChange={(v) => setValues(v as string[])}>
<ComboboxChips className="w-72">
{values.map((v) => {
const fw = multiFrameworks.find((f) => f.value === v)
return fw ? <ComboboxChip key={v}>{fw.label}</ComboboxChip> : null
})}
<ComboboxChipsInput placeholder="Add framework..." />
</ComboboxChips>
<ComboboxContent>
<ComboboxList>
<ComboboxEmpty>No framework found.</ComboboxEmpty>
<ComboboxGroup>
{multiFrameworks.map((fw) => (
<ComboboxItem key={fw.value} value={fw.value}>
{fw.label}
</ComboboxItem>
))}
</ComboboxGroup>
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}可展开多选
ReactQwik+3 more
tsx
import { useState } from "react"
import { Badge } from "@polyui/react/badge"
import { CircleCheckIcon, XIcon } from "lucide-react"
export function ComboboxMultipleExpandable() {
const [values, setValues] = useState<string[]>(["react", "qwik", "solidjs", "angular", "astro"])
const [expanded, setExpanded] = useState(false)
const maxShown = 2
const visible = expanded ? values : values.slice(0, maxShown)
const hiddenCount = values.length - visible.length
const removeValue = (v: string) => setValues((prev) => prev.filter((x) => x !== v))
const toggle = (v: string) => setValues((prev) => (prev.includes(v) ? prev.filter((x) => x !== v) : [...prev, v]))
const [open, setOpen] = useState(false)
return (
<div className="space-y-2">
<div
className="flex min-h-8 w-72 cursor-pointer flex-wrap items-center gap-1 rounded-lg border border-input bg-transparent px-2.5 py-1 text-sm"
onClick={() => setOpen((o) => !o)}
>
{values.length > 0 ? (
<>
{visible.map((v) => {
const fw = multiFrameworks.find((f) => f.value === v)
return fw ? (
<Badge key={v} variant="outline" className="rounded-sm gap-1">
{fw.label}
<button
onClick={(e) => {
e.stopPropagation()
removeValue(v)
}}
className="opacity-60 hover:opacity-100"
>
<XIcon className="size-3" />
</button>
</Badge>
) : null
})}
{(hiddenCount > 0 || expanded) && (
<Badge
variant="outline"
className="rounded-sm cursor-pointer"
onClick={(e) => {
e.stopPropagation()
setExpanded((x) => !x)
}}
>
{expanded ? "Show Less" : `+${hiddenCount} more`}
</Badge>
)}
</>
) : (
<span className="text-muted-foreground">Select framework</span>
)}
</div>
{open && (
<div className="w-72 rounded-md border bg-popover p-1 shadow-md">
{multiFrameworks.map((fw) => (
<button
key={fw.value}
className="flex w-full items-center justify-between rounded-sm px-2 py-1.5 text-sm hover:bg-accent"
onClick={() => toggle(fw.value)}
>
{fw.label}
{values.includes(fw.value) && <CircleCheckIcon className="size-4 fill-primary stroke-background" />}
</button>
))}
</div>
)}
</div>
)
}多选计数
tsx
import { useState } from "react"
import { Badge } from "@polyui/react/badge"
import { CircleCheckIcon } from "lucide-react"
export function ComboboxMultipleCount() {
const [values, setValues] = useState<string[]>(["react", "nextjs", "angular", "vue", "django", "astro"])
const [open, setOpen] = useState(false)
const toggle = (v: string) => setValues((prev) => (prev.includes(v) ? prev.filter((x) => x !== v) : [...prev, v]))
return (
<div className="space-y-2">
<button
className="flex min-h-8 w-72 items-center justify-between rounded-lg border border-input px-2.5 py-1 text-sm"
onClick={() => setOpen((o) => !o)}
>
{values.length > 0 ? (
<span>
<Badge variant="outline" className="rounded-sm mr-1">
{values.length}
</Badge>
frameworks selected
</span>
) : (
<span className="text-muted-foreground">Select framework</span>
)}
</button>
{open && (
<div className="w-72 rounded-md border bg-popover p-1 shadow-md">
{multiFrameworks.map((fw) => (
<button
key={fw.value}
className="flex w-full items-center justify-between rounded-sm px-2 py-1.5 text-sm hover:bg-accent"
onClick={() => toggle(fw.value)}
>
{fw.label}
{values.includes(fw.value) && <CircleCheckIcon className="size-4 fill-primary stroke-background" />}
</button>
))}
</div>
)}
</div>
)
}滑入
Menu slides in from bottom
tsx
import { useState } from "react"
import { Combobox, ComboboxInput, ComboboxContent, ComboboxList, ComboboxItem, ComboboxEmpty, ComboboxGroup } from "@polyui/react/combobox"
export function ComboboxSlideIn() {
const [value, setValue] = useState<string | null>(null)
return (
<div className="space-y-2">
<p className="text-sm text-muted-foreground">Menu slides in from bottom</p>
<Combobox value={value} onValueChange={setValue}>
<ComboboxInput placeholder="Select framework..." showTrigger className="w-64" />
<ComboboxContent className="data-[side=bottom]:slide-in-from-bottom-10 duration-300">
<ComboboxList>
<ComboboxEmpty>No framework found.</ComboboxEmpty>
<ComboboxGroup>
{frameworks.map((fw) => (
<ComboboxItem key={fw.value} value={fw.value}>
{fw.label}
</ComboboxItem>
))}
</ComboboxGroup>
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
)
}缩放动画
Menu zooms in from center
tsx
import { useState } from "react"
import { Combobox, ComboboxInput, ComboboxContent, ComboboxList, ComboboxItem, ComboboxEmpty, ComboboxGroup } from "@polyui/react/combobox"
export function ComboboxZoomIn() {
const [value, setValue] = useState<string | null>(null)
return (
<div className="space-y-2">
<p className="text-sm text-muted-foreground">Menu zooms in from center</p>
<Combobox value={value} onValueChange={setValue}>
<ComboboxInput placeholder="Select framework..." showTrigger className="w-64" />
<ComboboxContent className="origin-center duration-500 data-open:zoom-in-75">
<ComboboxList>
<ComboboxEmpty>No framework found.</ComboboxEmpty>
<ComboboxGroup>
{frameworks.map((fw) => (
<ComboboxItem key={fw.value} value={fw.value}>
{fw.label}
</ComboboxItem>
))}
</ComboboxGroup>
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
)
}属性
Combobox
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
value | string | string[] | — | 当前选中值(受控)。 |
onValueChange | (value: string | string[]) => void | — | 选中值变化回调。 |
multiple | boolean | false | 启用多选模式。 |
ComboboxInput
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
showTrigger | boolean | true | 是否显示右侧下拉箭头按钮。 |
showClear | boolean | false | 是否显示清除按钮。 |
placeholder | string | — | 输入框占位文本。 |
ComboboxContent
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
side | "top" | "bottom" | "left" | "right" | "bottom" | 下拉面板的展开方向。 |
sideOffset | number | 6 | 面板与触发元素的间距(px)。 |
align | "start" | "center" | "end" | "start" | 面板对齐方式。 |