HextaUI

Combobox

Un select filtrable con chips, grupos y resultados asíncronos, en un popup que cambia de tamaño mientras escribes.

"use client"

import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

const fruits = [
  "Apple",
  "Apricot",
  "Banana",
  "Blackberry",
  "Blueberry",
  "Cherry",
  "Grape",
  "Grapefruit",
  "Kiwi",
  "Lychee",
  "Mango",
  "Orange",
  "Papaya",
  "Peach",
  "Pear",
  "Pineapple",
  "Plum",
  "Raspberry",
  "Strawberry",
  "Watermelon",
]

export function ComboboxDemo() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="combobox-demo">Fruit</Label>
      <Combobox items={fruits}>
        <ComboboxInput id="combobox-demo" placeholder="Select a fruit" />
        <ComboboxContent>
          <ComboboxEmpty>No fruit found.</ComboboxEmpty>
          <ComboboxList>
            {(item: string) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/combobox.json

Añade el componente, los tokens del tema de HextaUI y los componentes de HextaUI de los que depende.

import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox"
const fruits = ["Apple", "Banana", "Cherry"]

<Combobox items={fruits}>
  <ComboboxInput placeholder="Select a fruit" />
  <ComboboxContent>
    <ComboboxEmpty>No fruit found.</ComboboxEmpty>
    <ComboboxList>
      {(item: string) => (
        <ComboboxItem key={item} value={item}>
          {item}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>

Pasa las opciones a items y renderiza cada una con una función dentro de <ComboboxList />. El combobox las filtra mientras escribes y solo renderiza las coincidencias. Los objetos también funcionan: su label se muestra en el input y su value se envía.

Escribe en el campo para filtrar la lista.

Combobox
├── ComboboxInput
└── ComboboxContent
    ├── ComboboxEmpty
    ├── ComboboxStatus
    └── ComboboxList
        ├── ComboboxItem
        ├── ComboboxGroup
        │   ├── ComboboxLabel
        │   └── ComboboxCollection
        │       └── ComboboxItem
        └── ComboboxSeparator

Un botón muestra el valor y el campo de búsqueda pasa al popup.

Combobox
├── ComboboxTrigger
│   └── ComboboxValue
└── ComboboxContent
    ├── ComboboxInput
    ├── ComboboxEmpty
    └── ComboboxList
        └── ComboboxItem

Con multiple, cada elemento seleccionado se convierte en un chip antes del input.

Combobox
├── ComboboxChips
│   └── ComboboxValue
│       ├── ComboboxChip
│       └── ComboboxChipsInput
└── ComboboxContent
    ├── ComboboxEmpty
    └── ComboboxList
        └── ComboboxItem

Botón de borrar

showClear añade un botón de borrar que ocupa el lugar del chevron mientras haya un valor, así que el campo nunca crece.

"use client"

import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

const fruits = [
  "Apple",
  "Apricot",
  "Banana",
  "Blackberry",
  "Blueberry",
  "Cherry",
  "Grape",
  "Grapefruit",
  "Kiwi",
  "Lychee",
  "Mango",
  "Orange",
  "Papaya",
  "Peach",
  "Pear",
  "Pineapple",
  "Plum",
  "Raspberry",
  "Strawberry",
  "Watermelon",
]

export function ComboboxWithClear() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="combobox-clear">Fruit</Label>
      <Combobox items={fruits} defaultValue="Mango">
        <ComboboxInput
          id="combobox-clear"
          placeholder="Select a fruit"
          showClear
        />
        <ComboboxContent>
          <ComboboxEmpty>No fruit found.</ComboboxEmpty>
          <ComboboxList>
            {(item: string) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}

Con iconos

Los iconos dentro de un elemento se dimensionan y atenúan por ti. autoHighlight resalta la primera coincidencia mientras se escribe, así que Enter la elige.

"use client"

import type * as React from "react"
import {
  IconBrandAngular,
  IconBrandNextjs,
  IconBrandReact,
  IconBrandSvelte,
  IconBrandVue,
} from "@tabler/icons-react"

import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

type Framework = {
  value: string
  label: string
  icon: React.ComponentType<{ className?: string }>
}

const frameworks: Framework[] = [
  { value: "next", label: "Next.js", icon: IconBrandNextjs },
  { value: "react", label: "React", icon: IconBrandReact },
  { value: "vue", label: "Vue", icon: IconBrandVue },
  { value: "svelte", label: "Svelte", icon: IconBrandSvelte },
  { value: "angular", label: "Angular", icon: IconBrandAngular },
]

export function ComboboxWithIcons() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="combobox-icons">Framework</Label>
      <Combobox items={frameworks} autoHighlight>
        <ComboboxInput id="combobox-icons" placeholder="Select a framework" />
        <ComboboxContent>
          <ComboboxEmpty>No framework found.</ComboboxEmpty>
          <ComboboxList>
            {(item: Framework) => (
              <ComboboxItem key={item.value} value={item}>
                <item.icon />
                {item.label}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}

Grupos y separadores

Pasa grupos con la forma { value, items } y renderiza cada uno con <ComboboxGroup />, <ComboboxLabel /> y <ComboboxCollection />. Los grupos vacíos se ocultan al filtrar.

"use client"

import * as React from "react"

import {
  Combobox,
  ComboboxCollection,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxGroup,
  ComboboxInput,
  ComboboxItem,
  ComboboxLabel,
  ComboboxList,
  ComboboxSeparator,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

type Timezone = { value: string; label: string }
type TimezoneGroup = { value: string; items: Timezone[] }

const timezones: TimezoneGroup[] = [
  {
    value: "Americas",
    items: [
      { value: "America/New_York", label: "New York (GMT-4)" },
      { value: "America/Chicago", label: "Chicago (GMT-5)" },
      { value: "America/Los_Angeles", label: "Los Angeles (GMT-7)" },
      { value: "America/Sao_Paulo", label: "São Paulo (GMT-3)" },
    ],
  },
  {
    value: "Europe",
    items: [
      { value: "Europe/London", label: "London (GMT+1)" },
      { value: "Europe/Paris", label: "Paris (GMT+2)" },
      { value: "Europe/Berlin", label: "Berlin (GMT+2)" },
    ],
  },
  {
    value: "Asia",
    items: [
      { value: "Asia/Kolkata", label: "Kolkata (GMT+5:30)" },
      { value: "Asia/Tokyo", label: "Tokyo (GMT+9)" },
      { value: "Asia/Singapore", label: "Singapore (GMT+8)" },
    ],
  },
]

export function ComboboxGroups() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="combobox-groups">Timezone</Label>
      <Combobox items={timezones} autoHighlight>
        <ComboboxInput id="combobox-groups" placeholder="Select a timezone" />
        <ComboboxContent>
          <ComboboxEmpty>No timezone found.</ComboboxEmpty>
          <ComboboxList>
            {(group: TimezoneGroup, index: number) => (
              <React.Fragment key={group.value}>
                {index > 0 ? <ComboboxSeparator /> : null}
                <ComboboxGroup items={group.items}>
                  <ComboboxLabel>{group.value}</ComboboxLabel>
                  <ComboboxCollection>
                    {(item: Timezone) => (
                      <ComboboxItem key={item.value} value={item}>
                        {item.label}
                      </ComboboxItem>
                    )}
                  </ComboboxCollection>
                </ComboboxGroup>
              </React.Fragment>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}

Múltiple

Con multiple, las selecciones se convierten en chips dentro de <ComboboxChips />. El popup permanece abierto mientras eliges, Backspace en el input vacío quita el último chip, y las teclas de flecha se mueven entre chips.

"use client"

import type * as React from "react"
import {
  IconBrandAngular,
  IconBrandNextjs,
  IconBrandReact,
  IconBrandSvelte,
  IconBrandVue,
} from "@tabler/icons-react"

import {
  Combobox,
  ComboboxChip,
  ComboboxChips,
  ComboboxChipsInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxItem,
  ComboboxList,
  ComboboxValue,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

type Framework = {
  value: string
  label: string
  icon: React.ComponentType<{ className?: string }>
}

const frameworks: Framework[] = [
  { value: "next", label: "Next.js", icon: IconBrandNextjs },
  { value: "react", label: "React", icon: IconBrandReact },
  { value: "vue", label: "Vue", icon: IconBrandVue },
  { value: "svelte", label: "Svelte", icon: IconBrandSvelte },
  { value: "angular", label: "Angular", icon: IconBrandAngular },
]

export function ComboboxMultiple() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="combobox-multiple">Frameworks</Label>
      <Combobox
        items={frameworks}
        multiple
        autoHighlight
        defaultValue={[frameworks[0], frameworks[1]]}
      >
        <ComboboxChips>
          <ComboboxValue>
            {(values: Framework[]) => (
              <>
                {values.map((value) => (
                  <ComboboxChip key={value.value}>{value.label}</ComboboxChip>
                ))}
                <ComboboxChipsInput
                  id="combobox-multiple"
                  placeholder={values.length > 0 ? "" : "Add frameworks"}
                />
              </>
            )}
          </ComboboxValue>
        </ComboboxChips>
        <ComboboxContent>
          <ComboboxEmpty>No framework found.</ComboboxEmpty>
          <ComboboxList>
            {(item: Framework) => (
              <ComboboxItem key={item.value} value={item}>
                <item.icon />
                {item.label}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}

Búsqueda dentro del popup

Usa <ComboboxTrigger /> para un campo tipo select. Pon el input dentro de <ComboboxContent /> y se convierte en un cuadro de búsqueda con un icono, y el popup se ensancha hasta al menos 15rem.

"use client"

import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxTrigger,
  ComboboxValue,
} from "@/components/ui/combobox"

const countries = [
  { value: "ar", label: "Argentina" },
  { value: "au", label: "Australia" },
  { value: "br", label: "Brazil" },
  { value: "ca", label: "Canada" },
  { value: "de", label: "Germany" },
  { value: "fr", label: "France" },
  { value: "in", label: "India" },
  { value: "jp", label: "Japan" },
  { value: "mx", label: "Mexico" },
  { value: "nl", label: "Netherlands" },
  { value: "uk", label: "United Kingdom" },
  { value: "us", label: "United States" },
]

export function ComboboxPopupSearch() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Combobox items={countries}>
        <ComboboxTrigger aria-label="Country">
          <ComboboxValue placeholder="Select a country" />
        </ComboboxTrigger>
        <ComboboxContent>
          <ComboboxInput placeholder="Search countries" />
          <ComboboxEmpty>No country found.</ComboboxEmpty>
          <ComboboxList>
            {(item: (typeof countries)[number]) => (
              <ComboboxItem key={item.value} value={item}>
                {item.label}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}

Trigger renderizado como Button

Pasa render al trigger para usar cualquier botón. El popup se ancla a él y conserva al menos su ancho.

"use client"

import { Button } from "@/components/ui/button"
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxTrigger,
  ComboboxValue,
} from "@/components/ui/combobox"

const countries = [
  { value: "ar", label: "Argentina" },
  { value: "au", label: "Australia" },
  { value: "br", label: "Brazil" },
  { value: "ca", label: "Canada" },
  { value: "de", label: "Germany" },
  { value: "fr", label: "France" },
  { value: "in", label: "India" },
  { value: "jp", label: "Japan" },
  { value: "mx", label: "Mexico" },
  { value: "nl", label: "Netherlands" },
  { value: "uk", label: "United Kingdom" },
  { value: "us", label: "United States" },
]

export function ComboboxButtonTrigger() {
  return (
    <Combobox items={countries} defaultValue={countries[6]}>
      <ComboboxTrigger
        aria-label="Country"
        render={<Button variant="outline" />}
      >
        <ComboboxValue />
      </ComboboxTrigger>
      <ComboboxContent>
        <ComboboxInput placeholder="Search countries" />
        <ComboboxEmpty>No country found.</ComboboxEmpty>
        <ComboboxList>
          {(item: (typeof countries)[number]) => (
            <ComboboxItem key={item.value} value={item}>
              {item.label}
            </ComboboxItem>
          )}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  )
}

Controlado

Controla la selección con value y onValueChange, y el popup con open y onOpenChange. Borrar establece el valor en null.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

const fruits = [
  "Apple",
  "Apricot",
  "Banana",
  "Blackberry",
  "Blueberry",
  "Cherry",
  "Grape",
  "Grapefruit",
  "Kiwi",
  "Lychee",
  "Mango",
  "Orange",
  "Papaya",
  "Peach",
  "Pear",
  "Pineapple",
  "Plum",
  "Raspberry",
  "Strawberry",
  "Watermelon",
]

export function ComboboxControlled() {
  const [value, setValue] = React.useState<string | null>("Peach")
  const [open, setOpen] = React.useState(false)

  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="combobox-controlled">Fruit</Label>
      <Combobox
        items={fruits}
        value={value}
        onValueChange={setValue}
        open={open}
        onOpenChange={setOpen}
      >
        <ComboboxInput
          id="combobox-controlled"
          placeholder="Select a fruit"
          showClear
        />
        <ComboboxContent>
          <ComboboxEmpty>No fruit found.</ComboboxEmpty>
          <ComboboxList>
            {(item: string) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
      <div className="flex items-center gap-2">
        <Button size="sm" variant="outline" onClick={() => setValue("Kiwi")}>
          Pick Kiwi
        </Button>
        <Button size="sm" variant="outline" onClick={() => setOpen(!open)}>
          {open ? "Close" : "Open"}
        </Button>
      </div>
      <p className="text-sm text-muted-foreground">Value: {value ?? "none"}</p>
    </div>
  )
}

Elementos deshabilitados, no válidos y deshabilitados

disabled en la raíz atenúa el campo y sus botones. aria-invalid en el input dibuja el anillo de error. Las teclas de flecha omiten los elementos deshabilitados.

"use client"

import {
  Combobox,
  ComboboxContent,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

const fruits = [
  "Apple",
  "Apricot",
  "Banana",
  "Blackberry",
  "Blueberry",
  "Cherry",
  "Grape",
  "Grapefruit",
  "Kiwi",
  "Lychee",
  "Mango",
  "Orange",
  "Papaya",
  "Peach",
  "Pear",
  "Pineapple",
  "Plum",
  "Raspberry",
  "Strawberry",
  "Watermelon",
]

export function ComboboxStates() {
  return (
    <div className="grid w-full max-w-xl gap-6 sm:grid-cols-2">
      <div className="flex flex-col gap-2">
        <Label htmlFor="combobox-disabled">Disabled</Label>
        <Combobox items={fruits} disabled defaultValue="Apple">
          <ComboboxInput id="combobox-disabled" showClear />
          <ComboboxContent>
            <ComboboxList>
              {(item: string) => (
                <ComboboxItem key={item} value={item}>
                  {item}
                </ComboboxItem>
              )}
            </ComboboxList>
          </ComboboxContent>
        </Combobox>
      </div>
      <div className="flex flex-col gap-2">
        <Label htmlFor="combobox-invalid">Invalid</Label>
        <Combobox items={fruits}>
          <ComboboxInput
            id="combobox-invalid"
            aria-invalid
            placeholder="Required"
          />
          <ComboboxContent>
            <ComboboxList>
              {(item: string) => (
                <ComboboxItem
                  key={item}
                  value={item}
                  disabled={item.startsWith("B")}
                >
                  {item}
                </ComboboxItem>
              )}
            </ComboboxList>
          </ComboboxContent>
        </Combobox>
        <p className="text-sm text-muted-foreground">
          Items starting with B are disabled.
        </p>
      </div>
    </div>
  )
}

Contenido largo y listas grandes

Las etiquetas largas y sin espacios se ajustan en lugar de ensanchar el popup. limit limita cuántas coincidencias se renderizan, lo que mantiene rápida una lista de 500 elementos.

"use client"

import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

const longItems = [
  "A very long option label that wraps onto a second line instead of pushing the popup wider than its input",
  "supercalifragilisticexpialidocious-unbroken-string-without-any-spaces-at-all-anywhere",
  "olivia.martin+newsletter-subscriptions@a-very-long-company-domain.example.com",
  "👩‍👩‍👧‍👦 Family 🧑🏽‍💻 Developer 🏳️‍🌈",
  "東京都千代田区丸の内一丁目",
  "",
  "Short",
]

const manyItems = Array.from({ length: 500 }, (_, index) => `Item ${index + 1}`)

export function ComboboxLongContent() {
  return (
    <div className="grid w-full max-w-xl gap-6 sm:grid-cols-2">
      <div className="flex w-full max-w-60 flex-col gap-2">
        <Label htmlFor="combobox-long">Long labels</Label>
        <Combobox items={longItems} defaultValue={longItems[1]}>
          <ComboboxInput id="combobox-long" placeholder="Pick one" showClear />
          <ComboboxContent>
            <ComboboxEmpty>
              Nothing matches this unusually long query, try something else.
            </ComboboxEmpty>
            <ComboboxList>
              {(item: string) => (
                <ComboboxItem key={item} value={item}>
                  {item || "(empty)"}
                </ComboboxItem>
              )}
            </ComboboxList>
          </ComboboxContent>
        </Combobox>
      </div>
      <div className="flex flex-col gap-2">
        <Label htmlFor="combobox-many">500 items</Label>
        <Combobox items={manyItems} limit={100}>
          <ComboboxInput id="combobox-many" placeholder="Search items" />
          <ComboboxContent>
            <ComboboxEmpty>No item found.</ComboboxEmpty>
            <ComboboxList>
              {(item: string) => (
                <ComboboxItem key={item} value={item}>
                  {item}
                </ComboboxItem>
              )}
            </ComboboxList>
          </ComboboxContent>
        </Combobox>
        <p className="text-sm text-muted-foreground">
          Shows the first 100 matches.
        </p>
      </div>
    </div>
  )
}

Desactiva el filtrado integrado con filter={null}, obtén los datos en onInputValueChange y muestra el progreso en <ComboboxStatus />, que lo anuncia a los lectores de pantalla. La altura del popup se anima a medida que cambian los resultados.

"use client"

import * as React from "react"
import { IconLoader2 } from "@tabler/icons-react"

import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxStatus,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

const countries = [
  { value: "ar", label: "Argentina" },
  { value: "au", label: "Australia" },
  { value: "br", label: "Brazil" },
  { value: "ca", label: "Canada" },
  { value: "de", label: "Germany" },
  { value: "fr", label: "France" },
  { value: "in", label: "India" },
  { value: "jp", label: "Japan" },
  { value: "mx", label: "Mexico" },
  { value: "nl", label: "Netherlands" },
  { value: "es", label: "Spain" },
  { value: "se", label: "Sweden" },
  { value: "ch", label: "Switzerland" },
  { value: "za", label: "South Africa" },
  { value: "kr", label: "South Korea" },
  { value: "uk", label: "United Kingdom" },
  { value: "us", label: "United States" },
]

export function ComboboxAsync() {
  const [query, setQuery] = React.useState("")
  const [results, setResults] = React.useState(countries.slice(0, 5))
  const [loading, setLoading] = React.useState(false)
  const runRef = React.useRef(0)

  React.useEffect(() => {
    const run = ++runRef.current
    const timer = setTimeout(() => {
      setLoading(true)
      setTimeout(() => {
        if (run !== runRef.current) {
          return
        }
        const needle = query.trim().toLowerCase()
        setResults(
          countries.filter((country) =>
            country.label.toLowerCase().includes(needle)
          )
        )
        setLoading(false)
      }, 600)
    }, 150)
    return () => {
      clearTimeout(timer)
      runRef.current += 1
    }
  }, [query])

  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="combobox-async">Country</Label>
      <Combobox
        items={results}
        filter={null}
        inputValue={query}
        onInputValueChange={setQuery}
      >
        <ComboboxInput id="combobox-async" placeholder="Search countries" />
        <ComboboxContent>
          <ComboboxStatus>
            {loading ? (
              <>
                <IconLoader2 className="animate-spin" />
                Searching…
              </>
            ) : null}
          </ComboboxStatus>
          {loading ? null : (
            <ComboboxEmpty>No country matches “{query}”.</ComboboxEmpty>
          )}
          <ComboboxList>
            {(item: (typeof countries)[number]) => (
              <ComboboxItem key={item.value} value={item}>
                {item.label}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}

Dentro de una hoja

El popup se apila sobre el sheet, y Escape cierra el popup antes que el sheet.

"use client"

import { Button } from "@/components/ui/button"
import {
  Combobox,
  ComboboxCollection,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxGroup,
  ComboboxInput,
  ComboboxItem,
  ComboboxLabel,
  ComboboxList,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"
import {
  Sheet,
  SheetBody,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

type Timezone = { value: string; label: string }
type TimezoneGroup = { value: string; items: Timezone[] }

const timezones: TimezoneGroup[] = [
  {
    value: "Americas",
    items: [
      { value: "America/New_York", label: "New York (GMT-4)" },
      { value: "America/Chicago", label: "Chicago (GMT-5)" },
      { value: "America/Los_Angeles", label: "Los Angeles (GMT-7)" },
      { value: "America/Sao_Paulo", label: "São Paulo (GMT-3)" },
    ],
  },
  {
    value: "Europe",
    items: [
      { value: "Europe/London", label: "London (GMT+1)" },
      { value: "Europe/Paris", label: "Paris (GMT+2)" },
      { value: "Europe/Berlin", label: "Berlin (GMT+2)" },
    ],
  },
  {
    value: "Asia",
    items: [
      { value: "Asia/Kolkata", label: "Kolkata (GMT+5:30)" },
      { value: "Asia/Tokyo", label: "Tokyo (GMT+9)" },
      { value: "Asia/Singapore", label: "Singapore (GMT+8)" },
    ],
  },
]

export function ComboboxInSheet() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Open sheet
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Preferences</SheetTitle>
          <SheetDescription>
            Choose the timezone for your reports.
          </SheetDescription>
        </SheetHeader>
        <SheetBody>
          <div className="flex flex-col gap-2">
            <Label htmlFor="combobox-sheet">Timezone</Label>
            <Combobox items={timezones} autoHighlight>
              <ComboboxInput
                id="combobox-sheet"
                placeholder="Select a timezone"
              />
              <ComboboxContent>
                <ComboboxEmpty>No timezone found.</ComboboxEmpty>
                <ComboboxList>
                  {(group: TimezoneGroup) => (
                    <ComboboxGroup key={group.value} items={group.items}>
                      <ComboboxLabel>{group.value}</ComboboxLabel>
                      <ComboboxCollection>
                        {(item: Timezone) => (
                          <ComboboxItem key={item.value} value={item}>
                            {item.label}
                          </ComboboxItem>
                        )}
                      </ComboboxCollection>
                    </ComboboxGroup>
                  )}
                </ComboboxList>
              </ComboboxContent>
            </Combobox>
          </div>
        </SheetBody>
      </SheetContent>
    </Sheet>
  )
}

De derecha a izquierda

El popup toma la dirección del campo, así que el botón de borrar, los chips y los elementos se reflejan sin props adicionales.

"use client"

import { DirectionProvider } from "@base-ui/react/direction-provider"

import {
  Combobox,
  ComboboxChip,
  ComboboxChips,
  ComboboxChipsInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxValue,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

const cities = ["القاهرة", "الرياض", "دبي", "بيروت", "عمّان", "الدوحة"]

export function ComboboxRtl() {
  return (
    <DirectionProvider direction="rtl">
      <div dir="rtl" className="flex w-full max-w-xs flex-col gap-4">
        <div className="flex flex-col gap-2">
          <Label htmlFor="combobox-rtl">المدينة</Label>
          <Combobox items={cities} defaultValue={cities[2]}>
            <ComboboxInput
              id="combobox-rtl"
              placeholder="اختر مدينة"
              showClear
            />
            <ComboboxContent>
              <ComboboxEmpty>لا توجد نتائج.</ComboboxEmpty>
              <ComboboxList>
                {(item: string) => (
                  <ComboboxItem key={item} value={item}>
                    {item}
                  </ComboboxItem>
                )}
              </ComboboxList>
            </ComboboxContent>
          </Combobox>
        </div>
        <div className="flex flex-col gap-2">
          <Label htmlFor="combobox-rtl-chips">المدن</Label>
          <Combobox items={cities} multiple defaultValue={[cities[0]]}>
            <ComboboxChips>
              <ComboboxValue>
                {(values: string[]) => (
                  <>
                    {values.map((value) => (
                      <ComboboxChip key={value}>{value}</ComboboxChip>
                    ))}
                    <ComboboxChipsInput id="combobox-rtl-chips" />
                  </>
                )}
              </ComboboxValue>
            </ComboboxChips>
            <ComboboxContent>
              <ComboboxList>
                {(item: string) => (
                  <ComboboxItem key={item} value={item}>
                    {item}
                  </ComboboxItem>
                )}
              </ComboboxList>
            </ComboboxContent>
          </Combobox>
        </div>
      </div>
    </DirectionProvider>
  )
}
KeyAcción
↓↑Abre el popup y mueve el resaltado por las coincidencias. Los elementos deshabilitados se omiten.
EnterElige el elemento resaltado. Sin nada resaltado, cierra el popup y deja que el formulario se envíe.
EscapeCierra el popup. Si ya está cerrado, borra el valor y el input.
HomeEndMueve el cursor de texto al inicio o al final del input.
BackspaceEn un input de chips vacío, quita el último chip. En un chip con foco, lo quita.
←→Con chips, mueve el foco entre chips y de vuelta al input. Reflejado en diseños de derecha a izquierda.
TabCierra el popup y mueve el foco.
  • Dale al input un <label> visible mediante id y htmlFor, o un aria-label. Un <ComboboxTrigger /> sin texto visible también necesita un aria-label.
  • El botón de chevron tiene la etiqueta “Show options”, el de borrar “Clear selection” y el de quitar de cada chip “Remove”.
  • El resaltado se mueve con aria-activedescendant, así que el foco permanece en el input mientras exploras.
  • Los inputs usan una fuente de 16px en pantallas táctiles para que iOS no haga zoom, y los elementos crecen hasta un objetivo táctil de 44px.

Construido sobre el combobox de Base UI. Cada parte acepta las props de la primitiva que envuelve; las tablas listan las que más usarás.

PropTipoPredeterminado
itemsLas opciones. Se filtran mientras escribes y se pasan a la función de renderizado de la lista.
Item[] | Group[]–
multipleSelecciona varios valores, mostrados como chips.
booleanfalse
value
Value | Value[] | null–
defaultValue
Value | Value[] | null–
onValueChange
(value, details) => void–
open
boolean–
defaultOpen
booleanfalse
onOpenChange
(open: boolean, details) => void–
inputValue
string–
defaultInputValue
string–
onInputValueChange
(inputValue: string, details) => void–
filterCoincidencia personalizada. null desactiva el filtrado para la búsqueda en el servidor.
((item, query, itemToString) => boolean) | null–
limitNúmero máximo de coincidencias a renderizar. -1 significa todas.
number-1
autoHighlightResalta la primera coincidencia mientras se escribe.
booleanfalse
highlightItemOnHover
booleantrue
openOnInputClick
booleantrue
loopFocusVuelve del último elemento al primero al mover el resaltado.
booleantrue
itemToStringLabelTexto que se muestra en el input para un elemento de tipo objeto.
(item) => string–
itemToStringValueValor que se envía con el formulario para un elemento de tipo objeto.
(item) => string–
isItemEqualToValue
(item, value) => boolean–
name
string–
required
booleanfalse
disabled
booleanfalse
readOnly
booleanfalse
modalBloquea el desplazamiento de la página y los clics exteriores mientras está abierto.
booleanfalse
virtualizedSe define al renderizar elementos con un virtualizador.
booleanfalse
localeConfiguración regional usada para las coincidencias.
Intl.LocalesArgument–

Fuera del popup renderiza el campo completo. Dentro de <ComboboxContent /> se convierte en un cuadro de búsqueda compacto.

PropTipoPredeterminado
showTriggerMuestra el botón de chevron. Siempre desactivado dentro del popup salvo que se defina.
booleantrue outside the popup
showClearMuestra un botón de borrar en lugar del chevron mientras haya un valor.
booleanfalse
classNameSe aplica al grupo de input que rodea al input.
string–
disabled
booleanfalse
placeholder
string–
AtributoDescripción
data-slot="combobox-input-group"El campo que rodea al input.
data-slot="combobox-input"El input de texto.
data-slot="combobox-input-actions"Contiene los botones de chevron y de borrar en una sola celda apilada.
data-popup-openPresente en el input mientras el popup está abierto.
data-popup-sideEl lado en el que se abrió el popup.
data-list-emptyPresente cuando nada coincide.
data-disabledPresente cuando está deshabilitado.
data-invalidPresente cuando no es válido dentro de un Field de Base UI.
PropTipoPredeterminado
childrenNormalmente un <ComboboxValue />. El chevron se añade después.
ReactNode–
renderCuando se define, se omiten los estilos de campo integrados.
ReactElement | (props, state) => ReactElement<button>
AtributoDescripción
data-slot="combobox-trigger"El botón trigger.
data-slot="combobox-trigger-value"Envuelve el valor truncado.
data-slot="combobox-trigger-icon"El chevron. Se voltea mientras está abierto.
data-popup-openPresente mientras el popup está abierto.
data-placeholderPresente mientras no hay ningún valor seleccionado.
PropTipoPredeterminado
childrenRenderiza tú mismo el valor seleccionado, por ejemplo como chips.
ReactNode | (value) => ReactNode–
placeholderSe muestra mientras no hay nada seleccionado.
ReactNode–
PropTipoPredeterminado
side
"top" | "bottom" | "left" | "right" | "inline-start" | "inline-end""bottom"
align
"start" | "center" | "end""start"
sideOffset
number6
alignOffset
number0
anchorPosiciona respecto a otro elemento. Por defecto, el campo. Consulta useComboboxAnchor.
Element | RefObject<Element | null> | VirtualElement | (() => Element | VirtualElement | null) | null–
dirPor defecto es la dirección del campo.
"ltr" | "rtl"–
AtributoDescripción
data-slot="combobox-positioner"Posiciona el popup.
data-slot="combobox-content"La superficie del popup.
data-slot="combobox-content-sizer"Se mide para animar la altura del popup a medida que cambian las coincidencias.
data-openPresente mientras está abierto.
data-sideEl lado en el que se abrió.
data-alignSu alineación.
data-emptyPresente cuando nada coincide.
data-starting-stylePresente mientras se anima la entrada.
data-ending-stylePresente mientras se anima la salida.
--combobox-item-radiusRadio del elemento, derivado del radio del popup menos su relleno.
PropTipoPredeterminado
childrenSe llama por cada coincidencia de items.
ReactNode | (item, index) => ReactNode–
AtributoDescripción
data-slot="combobox-list"La lista con desplazamiento.
PropTipoPredeterminado
valueEl elemento que representa esta fila.
Item–
disabled
booleanfalse
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescripción
data-slot="combobox-item"Una opción.
data-slot="combobox-item-indicator"La marca, que aparece escalando al seleccionarse.
data-highlightedPresente mientras está resaltado.
data-selectedPresente cuando está seleccionado.
data-disabledPresente cuando está deshabilitado.
PropTipoPredeterminado
itemsEn ComboboxGroup: los elementos propios del grupo.
Item[]–
childrenEn ComboboxCollection: renderiza cada coincidencia.
(item, index) => ReactNode–
AtributoDescripción
data-slot="combobox-group"Un grupo de elementos.
data-slot="combobox-label"El encabezado del grupo.

<ComboboxEmpty /> muestra a sus hijos solo cuando nada coincide. <ComboboxStatus /> es una región activa para mensajes de carga y de resultados. Ambos colapsan a nada cuando están vacíos.

AtributoDescripción
data-slot="combobox-empty"El mensaje de sin resultados.
data-slot="combobox-status"El mensaje de estado en vivo.
data-slot="combobox-separator"Un divisor entre grupos.
PropTipoPredeterminado
children
ReactNode<IconX />
AtributoDescripción
data-slot="combobox-clear"Etiquetado como “Clear selection”.
data-visiblePresente mientras hay algo que borrar.
PropTipoPredeterminado
classNameSe aplica al campo que envuelve los chips.
string–
AtributoDescripción
data-slot="combobox-chips"El campo que contiene los chips y el input.
PropTipoPredeterminado
showRemoveMuestra el botón de quitar.
booleantrue
AtributoDescripción
data-slot="combobox-chip"Un valor seleccionado.
data-slot="combobox-chip-label"Su etiqueta truncada.
data-slot="combobox-chip-remove"Etiquetado como “Remove”.

El input de texto que va después de los chips. Acepta las mismas props que el input de Base UI.

AtributoDescripción
data-slot="combobox-chips-input"El input de chips.
  • useComboboxAnchor() devuelve una ref para pasar a un elemento y a anchor en el contenido.
  • useComboboxFilter() devuelve comparadores contains, startsWith y endsWith conscientes de la configuración regional para filter.
  • useComboboxFilteredItems() lee las coincidencias actuales, para contadores o listas virtualizadas.
  • createComboboxItems(data, { getValue }) construye una colección de elementos cuyo valor de selección es un id primitivo, como una clave de base de datos, en lugar del objeto completo.
  • comboboxFieldVariants expone los estilos del campo para construir campos personalizados.