ComponentsForm
Combobox
A text field that filters a list as people type, so they choose one option, or several as chips, from many.
Import
import { Combobox, ComboboxInput, ComboboxContent, ComboboxList, ComboboxItem, ComboboxGroup, ComboboxLabel, ComboboxCollection, ComboboxEmpty, ComboboxStatus, ComboboxSeparator, ComboboxChips, ComboboxChip, ComboboxChipsInput, ComboboxTrigger, ComboboxClear, ComboboxValue } from "@booleanpress/ui/combobox"Also install @base-ui/react: pnpm add @base-ui/react
Usage
Combobox is Base UI's Combobox with shadcn's combobox parts and the library's field and list look. It needs the @base-ui/react peer package. The value must be one of the options: for free text with suggestions, use Autocomplete; for a short list with no typing, use Select.
import { Label } from "@booleanpress/ui/label"
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@booleanpress/ui/combobox"
const MAILERS = ["Amazon SES", "Mailgun", "Postmark", "SendGrid"]
export function MailerField() {
return (
<>
<Label htmlFor="mailer">Mailer</Label>
<Combobox items={MAILERS} defaultValue="Postmark">
<ComboboxInput id="mailer" placeholder="Search mailers" />
<ComboboxContent>
<ComboboxEmpty />
<ComboboxList>
{(mailer: string) => (
<ComboboxItem key={mailer} value={mailer}>
{mailer}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</>
)
}Pass the options to items and render each in ComboboxList's function child; Base UI filters them as people type (filter={null} turns that off, for results filtered on the server). An option's value is what value, defaultValue and onValueChange carry: a string, or an object, whose label (or itemToStringLabel) fills the field. It is uncontrolled with defaultValue, or controlled with value and onValueChange; null means nothing is chosen. When its form resets (a reset button, or React's reset after a form action), an uncontrolled combobox goes back to defaultValue and a controlled one is given the value it started with through onValueChange, as Select.
ComboboxInput is the field: as wide as its container, size sm, default or lg, variant="filled", both defaulting to the provider's controlSize and fieldVariant. showTrigger (on by default) adds the chevron cell, showClear a clear button while a value is chosen, loading a spinner. For several values, set multiple and build the field from ComboboxChips, ComboboxChip and ComboboxChipsInput, with useComboboxAnchor so the list lines up with the whole field. ComboboxEmpty says "No results" from the provider when nothing matches; ComboboxStatus announces loading and errors. Inside a Dialog, Sheet or Popover the list works as anywhere else: a click picks and the overlay stays open, the wheel scrolls the list, and Escape closes the list before the overlay.
Examples
Basic
A field that filters the mailers as you type; showTrigger={false} leaves out the chevron.
import { Label } from "@booleanpress/ui/label"
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@booleanpress/ui/combobox"
const MAILERS = ["Amazon SES", "Brevo", "Mailgun", "Mailjet", "Postmark", "Resend", "SendGrid", "SparkPost"]
export default function ComboboxBasic() {
return (
<div className="flex w-full max-w-56 flex-col gap-2">
<Label htmlFor="combobox-mailer">Mailer</Label>
<Combobox items={MAILERS}>
<ComboboxInput id="combobox-mailer" placeholder="Search mailers" showTrigger={false} />
<ComboboxContent>
<ComboboxEmpty />
<ComboboxList>
{(mailer: string) => (
<ComboboxItem key={mailer} value={mailer}>
{mailer}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
)
}With trigger button
The chevron cell, on by default, opens and closes the whole list; object options show their label.
import { Label } from "@booleanpress/ui/label"
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@booleanpress/ui/combobox"
const REGIONS = [
{ value: "us-east-1", label: "US East (N. Virginia)" },
{ value: "us-west-2", label: "US West (Oregon)" },
{ value: "eu-west-1", label: "Europe (Ireland)" },
{ value: "eu-central-1", label: "Europe (Frankfurt)" },
{ value: "ap-south-1", label: "Asia Pacific (Mumbai)" },
{ value: "ap-southeast-2", label: "Asia Pacific (Sydney)" },
]
export default function ComboboxTrigger() {
return (
<div className="flex w-full max-w-56 flex-col gap-2">
<Label htmlFor="combobox-region">Sending region</Label>
<Combobox items={REGIONS} defaultValue={REGIONS[2]}>
<ComboboxInput id="combobox-region" placeholder="Choose a region" />
<ComboboxContent>
<ComboboxEmpty />
<ComboboxList>
{(region: (typeof REGIONS)[number]) => (
<ComboboxItem key={region.value} value={region}>
{region.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
)
}Multiple
multiple with ComboboxChips: each choice is a chip in the field; Backspace in the empty input removes the last one.
import { Label } from "@booleanpress/ui/label"
import {
Combobox,
ComboboxChip,
ComboboxChips,
ComboboxChipsInput,
ComboboxContent,
ComboboxEmpty,
ComboboxItem,
ComboboxList,
ComboboxValue,
useComboboxAnchor,
} from "@booleanpress/ui/combobox"
const EVENTS = ["Delivered", "Bounced", "Complained", "Opened", "Clicked", "Unsubscribed", "Deferred", "Failed"]
export default function ComboboxMultiple() {
const anchor = useComboboxAnchor()
return (
<div className="flex w-full max-w-sm flex-col gap-2">
<Label htmlFor="combobox-events">Webhook events</Label>
<Combobox items={EVENTS} multiple defaultValue={["Bounced", "Complained"]}>
<ComboboxChips ref={anchor}>
<ComboboxValue>
{(events: string[]) => (
<>
{events.map((event) => (
<ComboboxChip key={event}>{event}</ComboboxChip>
))}
<ComboboxChipsInput id="combobox-events" placeholder={events.length ? "" : "Add events"} />
</>
)}
</ComboboxValue>
</ComboboxChips>
<ComboboxContent anchor={anchor}>
<ComboboxEmpty />
<ComboboxList>
{(event: string) => (
<ComboboxItem key={event} value={event}>
{event}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
)
}Custom option
icon and description give an option an avatar and a second line; itemToStringLabel fills the field with the name.
import { Avatar, AvatarFallback } from "@booleanpress/ui/avatar"
import { Label } from "@booleanpress/ui/label"
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@booleanpress/ui/combobox"
const CUSTOMERS = [
{ id: "c1", name: "Northwind Traders", email: "billing@northwind.example" },
{ id: "c2", name: "Fabrikam Studio", email: "ops@fabrikam.example" },
{ id: "c3", name: "Contoso Clinics", email: "it@contoso.example" },
{ id: "c4", name: "Tailspin Toys", email: "hello@tailspin.example" },
]
const initials = (name: string) => name.split(" ").map((word) => word[0]).join("")
export default function ComboboxCustomOption() {
return (
<div className="flex w-full max-w-64 flex-col gap-2">
<Label htmlFor="combobox-customer">Customer</Label>
<Combobox items={CUSTOMERS} itemToStringLabel={(customer: (typeof CUSTOMERS)[number]) => customer.name}>
<ComboboxInput id="combobox-customer" placeholder="Search customers" />
<ComboboxContent>
<ComboboxEmpty />
<ComboboxList>
{(customer: (typeof CUSTOMERS)[number]) => (
<ComboboxItem
key={customer.id}
value={customer}
description={customer.email}
icon={
<Avatar>
<AvatarFallback className="text-xs">{initials(customer.name)}</AvatarFallback>
</Avatar>
}
>
<span className="font-medium">{customer.name}</span>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
)
}Groups
Grouped items render with ComboboxGroup and ComboboxLabel; filtering keeps the groups that still match.
import { Label } from "@booleanpress/ui/label"
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxList,
} from "@booleanpress/ui/combobox"
const GROUPS = [
{ value: "API mailers", items: ["Amazon SES", "Brevo", "Mailgun", "Postmark", "SendGrid"] },
{ value: "SMTP", items: ["Gmail SMTP", "Outlook SMTP", "Zoho SMTP", "Custom SMTP"] },
]
export default function ComboboxGroups() {
return (
<div className="flex w-full max-w-56 flex-col gap-2">
<Label htmlFor="combobox-groups">Mailer</Label>
<Combobox items={GROUPS}>
<ComboboxInput id="combobox-groups" placeholder="Search mailers" />
<ComboboxContent>
<ComboboxEmpty />
<ComboboxList>
{(group: (typeof GROUPS)[number]) => (
<ComboboxGroup key={group.value} items={group.items}>
<ComboboxLabel>{group.value}</ComboboxLabel>
{group.items.map((mailer) => (
<ComboboxItem key={mailer} value={mailer}>
{mailer}
</ComboboxItem>
))}
</ComboboxGroup>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
)
}Clear
showClear shows a clear button while a value is chosen; it empties the field and keeps focus in it.
import { Label } from "@booleanpress/ui/label"
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@booleanpress/ui/combobox"
const CATEGORIES = ["Billing", "Delivery", "Integrations", "Account", "Deliverability", "API"]
export default function ComboboxClear() {
return (
<div className="flex w-full max-w-56 flex-col gap-2">
<Label htmlFor="combobox-category">Ticket category</Label>
<Combobox items={CATEGORIES} defaultValue="Delivery">
<ComboboxInput id="combobox-category" placeholder="Any category" showClear />
<ComboboxContent>
<ComboboxEmpty />
<ComboboxList>
{(category: string) => (
<ComboboxItem key={category} value={category}>
{category}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
)
}Async
Results load from a stand-in server after 600 ms: filter={null}, a spinner in the field and ComboboxStatus announcing the load; only the latest request fills the list, and a failed one says so.
import { useRef, useState } from "react"
import { Label } from "@booleanpress/ui/label"
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxStatus,
} from "@booleanpress/ui/combobox"
const ORGANISATIONS = ["Acme Logistics", "Blue Harbour Bank", "Cedar Health", "Delta Freight", "Evergreen Schools", "Fjord Energy"]
// Stands in for a request to the server: answers after 600 ms.
function searchOrganisations(query: string) {
return new Promise<string[]>((resolve) =>
setTimeout(() => resolve(ORGANISATIONS.filter((name) => name.toLowerCase().includes(query.toLowerCase()))), 600)
)
}
export default function ComboboxAsync() {
const [results, setResults] = useState<string[]>([])
const [loading, setLoading] = useState(false)
const [failed, setFailed] = useState(false)
// Only the latest request may update the list: an earlier, slower answer is dropped.
const latest = useRef(0)
async function search(query: string) {
const request = ++latest.current
setFailed(false)
if (!query.trim()) {
setResults([])
setLoading(false)
return
}
setLoading(true)
try {
const found = await searchOrganisations(query)
if (request === latest.current) setResults(found)
} catch {
if (request === latest.current) {
setResults([])
setFailed(true)
}
} finally {
if (request === latest.current) setLoading(false)
}
}
return (
<div className="flex w-full max-w-56 flex-col gap-2">
<Label htmlFor="combobox-organisation">Organisation</Label>
<Combobox items={results} filter={null} onInputValueChange={(query, { reason }) => reason !== "item-press" && search(query)}>
<ComboboxInput id="combobox-organisation" placeholder="Type to search" loading={loading} />
<ComboboxContent>
<ComboboxStatus loading={loading}>{failed ? "Could not load organisations. Try again." : null}</ComboboxStatus>
{!loading && !failed && <ComboboxEmpty />}
<ComboboxList>
{(name: string) => (
<ComboboxItem key={name} value={name}>
{name}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
)
}Virtualised
1,000 options, of which only the visible ones are in the page, with virtualized and @tanstack/react-virtual.
import { useImperativeHandle, useRef, useState, type RefObject } from "react"
import { useVirtualizer, type Virtualizer } from "@tanstack/react-virtual"
import { Label } from "@booleanpress/ui/label"
import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, useComboboxFilteredItems } from "@booleanpress/ui/combobox"
const LISTS = Array.from({ length: 1000 }, (_, index) => `Contact list ${String(index + 1).padStart(4, "0")}`)
function VirtualOptions({ open, virtualizer }: { open: boolean; virtualizer: RefObject<Virtualizer<HTMLDivElement, Element> | null> }) {
const items = useComboboxFilteredItems<string>()
const scroller = useRef<HTMLDivElement>(null)
// eslint-disable-next-line react-hooks/incompatible-library -- TanStack Virtual returns functions the compiler cannot memoise
const rows = useVirtualizer({ enabled: open, count: items.length, getScrollElement: () => scroller.current, estimateSize: () => 31, overscan: 10, paddingStart: 4, paddingEnd: 4 })
useImperativeHandle(virtualizer, () => rows)
if (!items.length) return null
return (
<div ref={scroller} role="presentation" className="max-h-[min(18rem,var(--available-height))] overflow-y-auto overscroll-contain">
<div role="presentation" className="relative" style={{ height: rows.getTotalSize() }}>
{rows.getVirtualItems().map((row) => (
<ComboboxItem
key={row.key}
index={row.index}
value={items[row.index]}
aria-setsize={items.length}
aria-posinset={row.index + 1}
className="absolute inset-x-1 w-auto"
style={{ top: 0, height: 29, transform: `translateY(${row.start}px)` }}
>
{items[row.index]}
</ComboboxItem>
))}
</div>
</div>
)
}
export default function ComboboxVirtualised() {
const [open, setOpen] = useState(false)
const virtualizer = useRef<Virtualizer<HTMLDivElement, Element> | null>(null)
return (
<div className="flex w-full max-w-56 flex-col gap-2">
<Label htmlFor="combobox-list">Contact list</Label>
<Combobox
items={LISTS}
virtualized
open={open}
onOpenChange={setOpen}
onItemHighlighted={(item, { reason, index }) => {
if (item && reason === "keyboard") queueMicrotask(() => virtualizer.current?.scrollToIndex(index, { align: "auto" }))
}}
>
<ComboboxInput id="combobox-list" placeholder="Search 1,000 lists" />
<ComboboxContent>
<ComboboxEmpty />
<ComboboxList className="overflow-visible p-0">
<VirtualOptions open={open} virtualizer={virtualizer} />
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
)
}Sizes
sm is 28 px high, default 35 px and lg 42 px; the chevron cell and icons scale with it.
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@booleanpress/ui/combobox"
const SIZES = [
{ size: "sm", label: "Small" },
{ size: "default", label: "Default" },
{ size: "lg", label: "Large" },
] as const
const REGIONS = ["Europe (Ireland)", "Europe (Frankfurt)", "US East (N. Virginia)", "Asia Pacific (Mumbai)"]
export default function ComboboxSizes() {
return (
<div className="flex w-full max-w-56 flex-col gap-4">
{SIZES.map(({ size, label }) => (
<Combobox key={size} items={REGIONS}>
<ComboboxInput size={size} placeholder={label} aria-label={`Region, ${label.toLowerCase()} size`} />
<ComboboxContent>
<ComboboxEmpty />
<ComboboxList>
{(region: string) => (
<ComboboxItem key={region} value={region}>
{region}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
))}
</div>
)
}Filled
variant="filled" fills the field with the grey --field-filled, on hover and focus too.
import { Label } from "@booleanpress/ui/label"
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@booleanpress/ui/combobox"
const TIME_ZONES = ["UTC", "Europe/London", "Europe/Berlin", "America/New_York", "America/Los_Angeles", "Asia/Dhaka"]
export default function ComboboxFilled() {
return (
<div className="flex w-full max-w-56 flex-col gap-2">
<Label htmlFor="combobox-zone">Report time zone</Label>
<Combobox items={TIME_ZONES}>
<ComboboxInput id="combobox-zone" variant="filled" placeholder="Search time zones" />
<ComboboxContent>
<ComboboxEmpty />
<ComboboxList>
{(zone: string) => (
<ComboboxItem key={zone} value={zone}>
{zone}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
)
}Disabled
disabled on the root and the input: the field keeps its value, fills grey and does not open.
import { Label } from "@booleanpress/ui/label"
import {
Combobox,
ComboboxContent,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@booleanpress/ui/combobox"
const MAILERS = ["Amazon SES", "Mailgun", "Postmark"]
export default function ComboboxDisabled() {
return (
<div className="flex w-full max-w-56 flex-col gap-2">
<Label htmlFor="combobox-fallback">Fallback mailer</Label>
<Combobox items={MAILERS} defaultValue="Postmark" disabled>
<ComboboxInput id="combobox-fallback" disabled />
<ComboboxContent>
<ComboboxList>
{(mailer: string) => (
<ComboboxItem key={mailer} value={mailer}>
{mailer}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
)
}Invalid
aria-invalid on the input draws the error edge and placeholder; aria-describedby reads the message with the name.
import { Label } from "@booleanpress/ui/label"
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@booleanpress/ui/combobox"
const MAILERS = ["Amazon SES", "Brevo", "Mailgun", "Postmark", "SendGrid"]
export default function ComboboxInvalid() {
return (
<div className="flex w-full max-w-56 flex-col gap-2">
<Label htmlFor="combobox-invalid">Mailer</Label>
<Combobox items={MAILERS} required>
<ComboboxInput id="combobox-invalid" placeholder="Choose a mailer" aria-invalid aria-describedby="combobox-invalid-error" />
<ComboboxContent>
<ComboboxEmpty />
<ComboboxList>
{(mailer: string) => (
<ComboboxItem key={mailer} value={mailer}>
{mailer}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
<p id="combobox-invalid-error" className="text-xs text-destructive-strong">
Choose a mailer to send the test email.
</p>
</div>
)
}In a dialog
Inside the library's Dialog the list may reach past the dialog's edge: a click picks and the dialog stays open, Escape closes the list first.
import { useState } from "react"
import { Button } from "@booleanpress/ui/button"
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@booleanpress/ui/combobox"
import {
Dialog,
DialogBody,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@booleanpress/ui/dialog"
import { Label } from "@booleanpress/ui/label"
const MAILERS = ["Amazon SES", "Brevo", "Mailgun", "Mailjet", "Postmark", "Resend", "SendGrid", "SparkPost"]
export default function ComboboxInDialog() {
const [mailer, setMailer] = useState<string | null>("Postmark")
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Route transactional email</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Route transactional email</DialogTitle>
<DialogDescription>Receipts and password resets go through this mailer.</DialogDescription>
</DialogHeader>
<DialogBody className="flex flex-col gap-2">
<Label htmlFor="combobox-route">Mailer</Label>
<Combobox items={MAILERS} value={mailer} onValueChange={setMailer}>
<ComboboxInput id="combobox-route" placeholder="Search mailers" />
<ComboboxContent>
<ComboboxEmpty />
<ComboboxList>
{(name: string) => (
<ComboboxItem key={name} value={name}>
{name}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</DialogBody>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button disabled={!mailer}>Save route</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}Accessibility
- Semantics
- The input has
role="combobox",aria-autocomplete="list",aria-expandedandaria-controlspointing at therole="listbox"list; the highlighted option is itsaria-activedescendant, and the chosen one hasaria-selected="true". Focus stays in the input while the list is open. A hidden input carries the value for forms. - Labels
- Name the input with a
LabelwhosehtmlForis itsid(or a label round the field),aria-labeloraria-labelledby. While the list is open, Base UI hides everything but the input and the list from assistive technology, the visible label included; so the input also carries its labels' text as its ownaria-label, read when it mounts, as the list opens and when the label's text changes, and keeps its name. An input named byaria-labeloraria-labelledbykeeps that name. The chevron is named "Show options" and the clear button "Clear", chip remove buttons "Remove {label}", all from the provider. Groups are named by theirComboboxLabel. - Focus
- The input is the only tab stop: the chevron, the clear button and the chip remove buttons are pointer targets (
tabindex="-1"), since the keyboard does the same from the input; a chip remove button's 22 px circle takes presses over a 24 px square, the minimum target of WCAG 2.5.8. Inmultiple, Left Arrow (Right Arrow on a right-to-left page) from the start of the input moves focus to the last chip, and the arrows move between the chips and back to the input. - Known limits
- The value must be an option. Free text needs
AutocompleteorTagsInput. - Typing filters with Base UI's locale-aware
containsmatch; passfilterfor another rule.
- The value must be an option. Free text needs
Keyboard
| Key | Behaviour |
|---|---|
| โ | Opens the list, then highlights the next option. |
| โ | Highlights the previous option. |
| Enter | Chooses the highlighted option, fills the field and closes the list. |
| Escape | Closes the list without choosing; inside a dialog, the dialog stays open. |
| HomeorEnd | Move the text cursor to the start or the end of the field, as in any text field. |
| AโZ | Typing filters the options; nothing matching shows "No results". |
| Backspace | With multiple, in an empty input, removes the last chip. |
API
Combobox
| Prop | Type | Default | Description |
|---|---|---|---|
actionsRef | RefObject<Actions | null> | A ref to imperative actions.
- unmount: Manually unmounts the combobox.
Call this after any externally controlled closing animation finishes. | |
autoComplete | string | Provides a hint to the browser for autofill. | |
autoHighlight | boolean | false | Whether the first matching item is highlighted automatically while filtering. |
defaultInputValue | string | number | readonly string[] | The uncontrolled input value when initially rendered.
To render a controlled input, use the inputValue prop instead. | |
defaultOpen | boolean | false | Whether the popup is initially open.
To render a controlled popup, use the open prop instead. |
defaultValue | ComboboxValueType<Value, Multiple> | null | The uncontrolled selected value of the combobox when it's initially rendered.
To render a controlled combobox, use the value prop instead. | |
disabled | boolean | false | Whether the component should ignore user interaction. |
filter | ((item: Item, query: string, itemToString?: ((item: Item) => string)) => boolean) | null | Filter function used to match items vs input query.
Receives the source item, which is the derived value's item when items is a createItems()
collection, and the item itself otherwise. | |
filteredItems | readonly Item[] | readonly Group<Item>[] | Filtered items to display in the list.
When provided, the list uses these items instead of filtering the items prop internally.
When items is also provided, this array must preserve its flat or grouped structure.
With a createItems() collection, pass source items rather than derived values.
Nullish entries are not supported, as in items.
Use when you want to control filtering logic externally with the useFilter() hook. | |
form | string | Identifies the form that owns the internal input. Useful when the combobox is rendered outside the form. | |
grid | boolean | false | Whether list items are presented in a grid layout. When enabled, arrow keys navigate across rows and columns inferred from DOM rows. |
highlightItemOnHover | boolean | true | Whether moving the pointer over items should highlight them.
Disabling this prop allows CSS :hover to be differentiated from the :focus (data-highlighted) state. |
id | string | The id of the component. | |
inline | boolean | false | Whether the list is rendered inline without using the component's own popup.
Specify open unconditionally in conjunction with this prop so the list is considered
visible: <Combobox.Root inline open>
In a Combobox.Root > Dialog.Root composition, bind the Combobox's open and
onOpenChange props to the Dialog's open and onOpenChange state instead so the
component resets its transient state (filter query, highlighted item, and input value) when
the dialog closes. |
inputRef | Ref<HTMLInputElement> | A ref to the hidden input element. | |
inputValue | string | number | readonly string[] | The input value of the combobox. Use when controlled. | |
isItemEqualToValue | ((itemValue: Value, value: Value) => boolean) | Custom comparison logic used to determine if a combobox item value matches the current selected value. Useful when item values are objects without matching referentially.
With a createItems() collection, both arguments are derived values.
Defaults to Object.is comparison. | |
items | readonly any[] | readonly Group<any>[] | ComboboxItemCollection<Item, Value> | The items to be displayed in the list.
Can be a flat array of items, an array of groups with items, or a collection created by
the createItems() function, which derives each item's selection value and label.
Nullish entries are not supported: remove them from the data before passing it. | |
itemToStringLabel | ((itemValue: Value) => string) | When the item values are objects (<Combobox.Item value={object}>), this function converts the object value to a string representation for display in the input.
If the shape of the object is { value, label }, the label will be used automatically without needing to specify this prop.
With a createItems() collection, this receives the derived value, and the collection's
getLabel takes precedence for values it can resolve. | |
itemToStringValue | ((itemValue: Value) => string) | When the item values are objects (<Combobox.Item value={object}>), this function converts the object value to a string representation for form submission.
If the shape of the object is { value, label }, the value will be used automatically without needing to specify this prop.
With a createItems() collection, this receives the derived value. | |
limit | number | -1 | The maximum number of items to display in the list. |
locale | LocalesArgument | The locale to use for string comparison. Defaults to the user's runtime locale. | |
loopFocus | boolean | true | Whether to loop keyboard focus back to the input when the end of the list is reached while using the arrow keys. The first item can then be reached by pressing <kbd>ArrowDown</kbd> again from the input, or the last item can be reached by pressing <kbd>ArrowUp</kbd> from the input. The input is always included in the focus loop per ARIA Authoring Practices. When disabled, focus does not move when on the last element and the user presses <kbd>ArrowDown</kbd>, or when on the first element and the user presses <kbd>ArrowUp</kbd>. |
modal | boolean | false | Determines if the popup enters a modal state when open.
- true: user interaction is limited to the popup: document page scroll is locked and pointer interactions on outside elements are disabled.
- false: user interaction with the rest of the document is allowed.
On touch devices, a true modal blocks outside taps but leaves the page scrollable unless the popup spans nearly the full viewport width, matching native iOS behavior. |
multiple | boolean | false | Whether multiple items can be selected. |
name | string | Identifies the field when a form is submitted. | |
onInputValueChange | ((inputValue: string, eventDetails: ChangeEventDetails) => void) | Event handler called when the input value changes. | |
onItemHighlighted | ((highlightedValue: Value, eventDetails: HighlightEventDetails) => void) | Callback fired when an item is highlighted or unhighlighted.
Receives the highlighted item value (or undefined if no item is highlighted) and event details with a reason property describing why the highlight changed.
The reason can be:
- 'keyboard': the highlight changed due to keyboard navigation.
- 'pointer': the highlight changed due to pointer hovering.
- 'none': the highlight changed programmatically. | |
onOpenChange | ((open: boolean, eventDetails: ChangeEventDetails) => void) | Event handler called when the popup is opened or closed. | |
onOpenChangeComplete | ((open: boolean) => void) | Event handler called after any animations complete when the popup is opened or closed. | |
onValueChange | ((value: ComboboxValueType<Value, Multiple> | (Multiple extends true ? never : null), eventDetails: ChangeEventDetails) => void) | Event handler called when the selected value of the combobox changes. | |
open | boolean | Whether the popup is currently open. Use when controlled. | |
openOnInputClick | boolean | true | Whether the popup opens when clicking the input. |
readOnly | boolean | false | Whether the user should be unable to choose a different option from the popup. |
required | boolean | false | Whether the user must choose a value before submitting a form. |
value | ComboboxValueType<Value, Multiple> | null | The selected value of the combobox. Use when controlled. | |
virtualized | boolean | false | Whether the items are being externally virtualized. |
ComboboxInput
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | ||
disabled | boolean | false | Whether the component should ignore user interaction. |
loading | boolean | false | Shows a spinner in the field while results load. Pair it with ComboboxStatus in the list, which announces it. |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxInputState> | Allows you to replace the component's HTML element
with a different tag, or compose it with another component.
Accepts a ReactElement or a function that returns the element to render. | |
showClear | boolean | false | Shows a clear button while a value is chosen; it empties the field and keeps focus in it. |
showTrigger | boolean | true | Shows the chevron button at the field's end, which opens and closes the list. |
size | "default" | "sm" | "lg" | 28, 35 or 42 px tall. Defaults to the provider's controlSize. | |
style | CSSProperties | ((state: ComboboxInputState) => CSSProperties) | Style applied to the element, or a function that returns a style object based on the component's state. | |
variant | "default" | "filled" | filled fills the field grey. Defaults to the provider's fieldVariant. |
ComboboxContent
| Prop | Type | Default | Description |
|---|---|---|---|
align | "center" | "start" | "end" | start | How to align the popup relative to the specified side. |
alignOffset | number | OffsetFunction | 0 | Additional offset along the alignment axis in pixels.
Also accepts a function that returns the offset to read the dimensions of the anchor
and positioner elements, along with its side and alignment.
The function takes a data object parameter with the following properties:
- data.anchor: the dimensions of the anchor element with properties width and height.
- data.positioner: the dimensions of the positioner element with properties width and height.
- data.side: which side of the anchor element the positioner is aligned against.
- data.align: how the positioner is aligned relative to the specified side. |
anchor | Element | VirtualElement | RefObject<Element | null> | (() => Element | VirtualElement | null) | null | An element to position the popup against. By default, the popup will be positioned against the trigger. | |
className | string | ||
container | HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null | A parent element to render the portal element into. | |
finalFocus | boolean | RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null) | Determines the element to focus when the popup is closed.
- false: Do not move focus.
- true: Move focus based on the default behavior (trigger or previously focused element).
- RefObject: Move focus to the ref element.
- function: Called with the interaction type (mouse, touch, pen, or keyboard).
Return an element to focus, true to use the default behavior, or false/undefined to do nothing. | |
initialFocus | boolean | RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | void | HTMLElement | null) | Determines the element to focus when the popup is opened.
- false: Do not move focus.
- true: Move focus based on the default behavior (first tabbable element or popup).
- RefObject: Move focus to the ref element.
- function: Called with the interaction type (mouse, touch, pen, or keyboard).
Return an element to focus, true to use the default behavior, or false/undefined to do nothing. | |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxPopupState> | Allows you to replace the component's HTML element
with a different tag, or compose it with another component.
Accepts a ReactElement or a function that returns the element to render. | |
side | "top" | "bottom" | "left" | "right" | "inline-end" | "inline-start" | bottom | Which side of the anchor element to align the popup against. May automatically change to avoid collisions. |
sideOffset | number | OffsetFunction | 2 | Distance between the anchor and the popup in pixels.
Also accepts a function that returns the distance to read the dimensions of the anchor
and positioner elements, along with its side and alignment.
The function takes a data object parameter with the following properties:
- data.anchor: the dimensions of the anchor element with properties width and height.
- data.positioner: the dimensions of the positioner element with properties width and height.
- data.side: which side of the anchor element the positioner is aligned against.
- data.align: how the positioner is aligned relative to the specified side. |
style | CSSProperties | ((state: ComboboxPopupState) => CSSProperties) | Style applied to the element, or a function that returns a style object based on the component's state. |
ComboboxList
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | ||
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxListState> | Allows you to replace the component's HTML element
with a different tag, or compose it with another component.
Accepts a ReactElement or a function that returns the element to render. | |
style | CSSProperties | ((state: ComboboxListState) => CSSProperties) | Style applied to the element, or a function that returns a style object based on the component's state. |
ComboboxItem
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | ||
description | ReactNode | A second, muted line under the label. | |
disabled | boolean | false | Whether the component should ignore user interaction. |
icon | ReactNode | Leading media beside both lines, such as an avatar or a flag. | |
index | number | The index of the item in the list. Improves performance when specified by avoiding the need to calculate the index automatically from the DOM. | |
nativeButton | boolean | true | Whether the component renders a native <button> element when replacing it
via the render prop.
Set to false if the rendered element is not a button (for example, <div>). |
onClick | ((event: BaseUIEvent<MouseEvent<HTMLDivElement, MouseEvent>>) => void) | An optional click handler for the item when selected.
It fires when clicking the item with the pointer, as well as when pressing Enter with the keyboard if the item is highlighted when the Input or List element has focus. | |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxItemState> | Allows you to replace the component's HTML element
with a different tag, or compose it with another component.
Accepts a ReactElement or a function that returns the element to render. | |
style | CSSProperties | ((state: ComboboxItemState) => CSSProperties) | Style applied to the element, or a function that returns a style object based on the component's state. | |
value | any | null | A unique value that identifies this item. |
ComboboxGroup
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | ||
items | readonly any[] | Items to be rendered within this group.
When provided, child Collection components will use these items. | |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxGroupState> | Allows you to replace the component's HTML element
with a different tag, or compose it with another component.
Accepts a ReactElement or a function that returns the element to render. | |
style | CSSProperties | ((state: ComboboxGroupState) => CSSProperties) | Style applied to the element, or a function that returns a style object based on the component's state. |
ComboboxLabel
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | ||
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxGroupLabelState> | Allows you to replace the component's HTML element
with a different tag, or compose it with another component.
Accepts a ReactElement or a function that returns the element to render. | |
style | CSSProperties | ((state: ComboboxGroupLabelState) => CSSProperties) | Style applied to the element, or a function that returns a style object based on the component's state. |
ComboboxCollection
ComboboxEmpty
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | ||
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxEmptyState> | Allows you to replace the component's HTML element
with a different tag, or compose it with another component.
Accepts a ReactElement or a function that returns the element to render. | |
style | CSSProperties | ((state: ComboboxEmptyState) => CSSProperties) | Style applied to the element, or a function that returns a style object based on the component's state. |
ComboboxStatus
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | ||
loading | boolean | false | Shows a spinner and the provider's loadingResults string in place of the children. |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxStatusState> | Allows you to replace the component's HTML element
with a different tag, or compose it with another component.
Accepts a ReactElement or a function that returns the element to render. | |
style | CSSProperties | ((state: ComboboxStatusState) => CSSProperties) | Style applied to the element, or a function that returns a style object based on the component's state. |
ComboboxSeparator
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | ||
orientation | "horizontal" | "vertical" | 'horizontal' | The orientation of the separator. |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxSeparatorState> | Allows you to replace the component's HTML element
with a different tag, or compose it with another component.
Accepts a ReactElement or a function that returns the element to render. | |
style | CSSProperties | ((state: ComboboxSeparatorState) => CSSProperties) | Style applied to the element, or a function that returns a style object based on the component's state. |
ComboboxChips
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | ||
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxChipsState> | Allows you to replace the component's HTML element
with a different tag, or compose it with another component.
Accepts a ReactElement or a function that returns the element to render. | |
size | "default" | "sm" | "lg" | 28, 35 or 42 px tall while it holds one row. Defaults to the provider's controlSize. | |
style | CSSProperties | ((state: ComboboxChipsState) => CSSProperties) | Style applied to the element, or a function that returns a style object based on the component's state. | |
variant | "default" | "filled" | filled fills the field grey. Defaults to the provider's fieldVariant. |
ComboboxChip
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | ||
label | string | The chip's text for the remove button's name, "Remove {label}", when its children are not plain text. | |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxChipState> | Allows you to replace the component's HTML element
with a different tag, or compose it with another component.
Accepts a ReactElement or a function that returns the element to render. | |
showRemove | boolean | true | Shows the remove button. On by default. |
style | CSSProperties | ((state: ComboboxChipState) => CSSProperties) | Style applied to the element, or a function that returns a style object based on the component's state. |
ComboboxChipsInput
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | ||
disabled | boolean | false | Whether the component should ignore user interaction. |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxInputState> | Allows you to replace the component's HTML element
with a different tag, or compose it with another component.
Accepts a ReactElement or a function that returns the element to render. | |
style | CSSProperties | ((state: ComboboxInputState) => CSSProperties) | Style applied to the element, or a function that returns a style object based on the component's state. |
ComboboxTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | ||
disabled | boolean | false | Whether the component should ignore user interaction. |
nativeButton | boolean | true | Whether the component renders a native <button> element when replacing it
via the render prop.
Set to false if the rendered element is not a button (for example, <div>). |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxTriggerState> | Allows you to replace the component's HTML element
with a different tag, or compose it with another component.
Accepts a ReactElement or a function that returns the element to render. | |
style | CSSProperties | ((state: ComboboxTriggerState) => CSSProperties) | Style applied to the element, or a function that returns a style object based on the component's state. |
ComboboxClear
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | ||
disabled | boolean | false | Whether the component should ignore user interaction. |
keepMounted | boolean | false | Whether the component should remain mounted in the DOM when not visible. |
nativeButton | boolean | true | Whether the component renders a native <button> element when replacing it
via the render prop.
Set to false if the rendered element is not a button (for example, <div>). |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxClearState> | Allows you to replace the component's HTML element
with a different tag, or compose it with another component.
Accepts a ReactElement or a function that returns the element to render. | |
style | CSSProperties | ((state: ComboboxClearState) => CSSProperties) | Style applied to the element, or a function that returns a style object based on the component's state. |
ComboboxValue
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | ((selectedValue: any) => ReactNode) | Accepts a function that returns a ReactNode to format the selected value.
Treat the value as read-only: in multiple mode it may be a shared frozen array
when nothing is selected. | |
placeholder | ReactNode | The placeholder value to display when no value is selected.
This is overridden by children if specified, or by a null item's label in items. |
Also exported: useComboboxAnchor, a hook for the parts' shared state; call it inside the component's provider; useComboboxFilteredItems, a hook for the parts' shared state; call it inside the component's provider.
Every part takes className, merged with its defaults by cn(), and ref, which reaches the element it renders.
Data attributes: data-slot="combobox-loading" (ComboboxInput), data-slot="combobox-content" (ComboboxContent), data-slot="combobox-list" (ComboboxList), data-slot="combobox-item" (ComboboxItem), data-slot="combobox-group" (ComboboxGroup), data-slot="combobox-label" (ComboboxLabel), data-slot="combobox-collection" (ComboboxCollection), data-slot="combobox-empty" (ComboboxEmpty), data-slot="combobox-status" (ComboboxStatus), data-slot="combobox-separator" (ComboboxSeparator), data-slot="combobox-chips" (ComboboxChips), data-slot="combobox-chip" (ComboboxChip), data-slot="combobox-chip-input" (ComboboxChipsInput), data-slot="combobox-trigger" (ComboboxTrigger), data-slot="combobox-clear" (ComboboxClear), data-slot="combobox-value" (ComboboxValue), and data-disabled, data-chips, data-size, data-variant.
Provider strings: toggleOptions, clear, noResults, loadingResults, removeItem (BooleanUIProvider's strings).
Theming
The field is InputGroup: --field (--field-filled with variant="filled"), --control for the edge (--control-hover under the pointer), --ring on focus and --invalid when invalid; the chevron and clear icons are --control-hover. The list uses --popover, --accent for the highlighted option and --highlight for the chosen one (--highlight-focus while highlighted). Chips use --secondary. The list's motion is set once in theme.css.
The tokens its classes read. Change their values in your theme, and it follows.
| Token | Used for |
|---|---|
--accent | background |
--accent-foreground | text |
--border | border, background |
--control | border |
--control-hover | text, border |
--destructive-strong | text |
--field | background |
--field-disabled | background |
--field-disabled-foreground | text |
--field-filled | background |
--foreground | text |
--highlight | background |
--highlight-focus | background |
--highlight-foreground | text |
--invalid | border |
--muted-foreground | text |
--popover | background |
--popover-foreground | text |
--ring | border |
--secondary | background |
--secondary-hover | background |