Combobox
Searchable input that filters a list of options. Built on Base UI Combobox.
const frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"];export function ComboboxBasicDemo() { return ( <Combobox items={frameworks}> <ComboboxInput placeholder="Select a framework" className="w-80 max-w-full" /> <ComboboxContent> <ComboboxEmpty>No items found.</ComboboxEmpty> <ComboboxList> {(item) => ( <ComboboxItem key={item} value={item}> {item} </ComboboxItem> )} </ComboboxList> </ComboboxContent> </Combobox> );}Usage
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@reva/ui";The trigger reuses Reva's Input / InputGroup so it inherits the plain-surface form-field look (no always-on heavy border) and feels like a sibling of Select. Multi-select uses ComboboxChips with ComboboxChip and ComboboxChipsInput instead of ComboboxInput.
ComboboxInput and ComboboxChips both expose a typed size prop ("sm" | "default" | "lg" | "xl") aligned with Input and InputGroup — see Sizes.
Composition
Simple
A single-line input and a flat list (see Basic).
Combobox
├── ComboboxInput
└── ComboboxContent
├── ComboboxEmpty
└── ComboboxList
├── ComboboxItem
└── ComboboxItemWith chips
Multi-select with multiple, chips, and a chips input (see Multiple).
Combobox
├── ComboboxChips
│ ├── ComboboxValue
│ │ └── ComboboxChip
│ └── ComboboxChipsInput
└── ComboboxContent
├── ComboboxEmpty
└── ComboboxList
├── ComboboxItem
└── ComboboxItemWith groups and collection
Nested items per group using ComboboxCollection inside each ComboboxGroup, with a separator between groups (see Groups).
Combobox
├── ComboboxInput
└── ComboboxContent
├── ComboboxEmpty
└── ComboboxList
├── ComboboxGroup
│ ├── ComboboxLabel
│ └── ComboboxCollection
│ ├── ComboboxItem
│ └── ComboboxItem
├── ComboboxSeparator
└── ComboboxGroup
├── ComboboxLabel
└── ComboboxCollection
├── ComboboxItem
└── ComboboxItemExamples
Each example below is a client component because Base UI's function-children pattern ({(item) => <ComboboxItem … />}) cannot cross the React Server Component boundary directly from MDX. Demo sources live in apps/docs/src/demos/combobox-demos.tsx.
Basic
A simple combobox over a flat string list.
<Combobox items={frameworks}> <ComboboxInput placeholder="Select a framework" className="w-80 max-w-full" /> <ComboboxContent> <ComboboxEmpty>No items found.</ComboboxEmpty> <ComboboxList> {(item) => ( <ComboboxItem key={item} value={item}> {item} </ComboboxItem> )} </ComboboxList> </ComboboxContent></Combobox>Manual entry (free-text)
Pass freeText to let the field commit a value that isn't in the list. The Combobox stays controlled with a plain string value / onValueChange, and the typed text survives closing the popup. Place a ComboboxCreate row inside ComboboxList (alongside the ComboboxCollection) so it is a real list item — clickable, keyboard-navigable, and committed with Enter. It appears only when the query has no exact match, and shows an Enter hint on the right.
const providers = ["Monzo", "Starling Bank", "HSBC", "Nationwide Building Society", "Vanguard"];export function ComboboxFreeTextDemo() { const [value, setValue] = React.useState(""); return ( <Combobox freeText value={value} onValueChange={setValue} items={providers}> <ComboboxInput placeholder="Search or type a provider" className="w-80 max-w-full" showClear /> <ComboboxContent> <ComboboxList> <ComboboxCollection> {(item) => ( <ComboboxItem key={item} value={item}> {item} </ComboboxItem> )} </ComboboxCollection> <ComboboxCreate /> </ComboboxList> </ComboboxContent> </Combobox> );}Inside a Dialog, pass the dialog's content element as container and set positionMethod="fixed" on ComboboxContent, so the popup portals into the dialog's scroll-lock subtree (otherwise the list can't be wheel-scrolled) and escapes the dialog's overflow clip:
const [dialogEl, setDialogEl] = useState<HTMLElement | null>(null);
<DialogContent ref={setDialogEl}>
<Combobox freeText value={value} onValueChange={setValue} items={items}>
<ComboboxInput placeholder="Provider" showClear />
<ComboboxContent container={dialogEl} positionMethod="fixed">
<ComboboxList>
<ComboboxCollection>
{(item) => <ComboboxItem key={item} value={item}>{item}</ComboboxItem>}
</ComboboxCollection>
<ComboboxCreate />
</ComboboxList>
</ComboboxContent>
</Combobox>
</DialogContent>Sizes
Pass size on the Combobox root to scale the trigger and the dropdown items together. The ramp matches Input and InputGroup: sm (32px), default (40px), lg (44px), xl (48px). Items inside ComboboxContent automatically pick up the size from context — no need to pass size to children.
<VStack gap={3}> {(["sm", "default", "lg", "xl"] as const).map((size) => ( <Combobox key={size} size={size} items={frameworks}> <ComboboxInput placeholder={`Size: ${size}`} className="w-80 max-w-full" /> <ComboboxContent> <ComboboxEmpty>No items found.</ComboboxEmpty> <ComboboxList> {(item) => ( <ComboboxItem key={item} value={item}> {item} </ComboboxItem> )} </ComboboxList> </ComboboxContent> </Combobox> ))}</VStack>Multiple
Pass multiple and render the selection as chips inside ComboboxChips. useComboboxAnchor anchors the popover to the chips wrapper so the dropdown grows to its width.
function ComboboxMultipleDemo() { const anchor = useComboboxAnchor(); return ( <Combobox multiple autoHighlight items={frameworks} defaultValue={[frameworks[0]]} > <ComboboxChips ref={anchor} className="w-80 max-w-full"> <ComboboxValue> {(values) => ( <> {values.map((value) => ( <ComboboxChip key={value}>{value}</ComboboxChip> ))} <ComboboxChipsInput placeholder="Add framework" /> </> )} </ComboboxValue> </ComboboxChips> <ComboboxContent anchor={anchor}> <ComboboxEmpty>No items found.</ComboboxEmpty> <ComboboxList> {(item) => ( <ComboboxItem key={item} value={item}> {item} </ComboboxItem> )} </ComboboxList> </ComboboxContent> </Combobox> );}Clear Button
Use the showClear prop on ComboboxInput to render a clear button inside the trigger when the field has a value.
<Combobox items={frameworks} defaultValue={frameworks[0]}> <ComboboxInput placeholder="Select a framework" showClear className="w-80 max-w-full" /> <ComboboxContent> <ComboboxEmpty>No items found.</ComboboxEmpty> <ComboboxList> {(item) => ( <ComboboxItem key={item} value={item}> {item} </ComboboxItem> )} </ComboboxList> </ComboboxContent></Combobox>Groups
Wrap related items in a ComboboxGroup with a ComboboxLabel and a ComboboxCollection. Pass each group's items to ComboboxGroup items={…} so filtering keeps the group together.
<Combobox items={timezones}> <ComboboxInput placeholder="Select a timezone" className="w-80 max-w-full" /> <ComboboxContent> <ComboboxEmpty>No timezones found.</ComboboxEmpty> <ComboboxList> {(group) => ( <ComboboxGroup key={group.value} items={group.items}> <ComboboxLabel>{group.value}</ComboboxLabel> <ComboboxCollection> {(item) => ( <ComboboxItem key={item} value={item}> {item} </ComboboxItem> )} </ComboboxCollection> <ComboboxSeparator /> </ComboboxGroup> )} </ComboboxList> </ComboboxContent></Combobox>Custom Items
Render arbitrary content inside ComboboxItem. When items are objects, pass itemToStringValue so the input filter sees a string.
<Combobox items={countries} itemToStringValue={(country) => country.label}> <ComboboxInput placeholder="Search countries…" className="w-80 max-w-full" /> <ComboboxContent> <ComboboxEmpty>No countries found.</ComboboxEmpty> <ComboboxList> {(country) => ( <ComboboxItem key={country.code} value={country}> <Item size="xs" className="p-0"> <ItemContent> <ItemTitle className="whitespace-nowrap">{country.label}</ItemTitle> <ItemDescription>{country.continent} ({country.code})</ItemDescription> </ItemContent> </Item> </ComboboxItem> )} </ComboboxList> </ComboboxContent></Combobox>Invalid
Set aria-invalid on ComboboxInput for the standalone error look, or wrap in Field with data-invalid to compose label, description, and error copy.
Please select a valid framework.
<VStack gap={4}> <Combobox items={frameworks}> <ComboboxInput placeholder="Select a framework" aria-invalid="true" className="w-80 max-w-full" /> <ComboboxContent> <ComboboxEmpty>No items found.</ComboboxEmpty> <ComboboxList> {(item) => ( <ComboboxItem key={item} value={item}> {item} </ComboboxItem> )} </ComboboxList> </ComboboxContent> </Combobox> <Field data-invalid> <FieldLabel htmlFor="combobox-framework-invalid">Framework</FieldLabel> <Combobox items={frameworks}> <ComboboxInput id="combobox-framework-invalid" placeholder="Select a framework" aria-invalid className="w-80 max-w-full" /> <ComboboxContent> <ComboboxEmpty>No items found.</ComboboxEmpty> <ComboboxList> {(item) => ( <ComboboxItem key={item} value={item}> {item} </ComboboxItem> )} </ComboboxList> </ComboboxContent> </Combobox> <FieldDescription>Please select a valid framework.</FieldDescription> <FieldError errors={[{ message: "This field is required." }]} /> </Field></VStack>Props
Combobox and its subcomponents forward props to Base UI Combobox. See the Base UI Combobox documentation for the full prop surface (items, value, onValueChange, multiple, filter, mode, autoHighlight, etc.).
| Prop | Type | Default | Description |
|---|---|---|---|
items | T[] | — | Source list passed to Combobox for filtering. |
value / defaultValue | T | T[] | — | Controlled vs uncontrolled selection. |
onValueChange | (value: T | T[]) => void | — | Fires when the selection changes. |
multiple | boolean | false | Enables multi-select with chips. |
freeText | boolean | false | Opt-in single-select free-text: value / onValueChange become a plain string and a typed value not in items is committed verbatim. Controlled only. |
itemToStringValue | (item: T) => string | — | String accessor for object items so the input filter sees a string. |
autoHighlight | boolean | false | Highlights the first match as the user types. |
disabled | boolean | false | Disables the control. |
ComboboxInput / ComboboxChips
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "default" | "lg" | "xl" | "default" | Control scale (32 / 40 / 44 / 48px). Aligned with Input and InputGroup. |
showTrigger | boolean | true | Show the chevron trigger button (ComboboxInput only). |
showClear | boolean | false | Show a clear button when the value is non-empty (ComboboxInput only). |
aria-invalid | boolean | "true" | — | Renders the error border / focus ring. |
ComboboxContent
| Prop | Type | Default | Description |
|---|---|---|---|
container | HTMLElement | null | — | Portal target for the popup. Inside a Dialog, pass the dialog content node so the list sits in the dialog's scroll-lock subtree and stays wheel-scrollable. |
positionMethod | "absolute" | "fixed" | "absolute" | Use "fixed" inside a Dialog so the popup escapes the dialog's overflow clip. |
ComboboxCreate
The explicit free-text create row (use with freeText). Place it inside ComboboxList, next to the ComboboxCollection, so it joins the keyboard-navigable list — clicking it or pressing Enter commits the typed value. Renders only when the query is non-empty and has no exact match, with a trailing Kbd "Enter". Accepts a render-function child (query: string) => ReactNode (default Use "<query>") and an optional icon.