Skip to main content
PolyUI/docs

表单组件

组合框

可搜索的下拉选择框,基于 @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>
  )
}

多选

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

属性类型默认值说明
valuestring | string[]当前选中值(受控)。
onValueChange(value: string | string[]) => void选中值变化回调。
multiplebooleanfalse启用多选模式。

ComboboxInput

属性类型默认值说明
showTriggerbooleantrue是否显示右侧下拉箭头按钮。
showClearbooleanfalse是否显示清除按钮。
placeholderstring输入框占位文本。

ComboboxContent

属性类型默认值说明
side"top" | "bottom" | "left" | "right""bottom"下拉面板的展开方向。
sideOffsetnumber6面板与触发元素的间距(px)。
align"start" | "center" | "end""start"面板对齐方式。