apps/docs/content/docs/react/components/(pickers)/combo-box.mdx
import { ComboBox } from '@heroui/react';
<ComponentPreview name="combo-box-default" />
Import the ComboBox component and access all parts using dot notation.
import { ComboBox, Input, Label, Description, Header, ListBox, Separator } from '@heroui/react';
export default () => (
<ComboBox>
<Label />
<ComboBox.InputGroup>
<Input />
<ComboBox.Trigger />
</ComboBox.InputGroup>
<Description />
<ComboBox.Popover>
<ListBox>
<ListBox.Item>
<Label />
<Description />
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Section>
<Header />
<ListBox.Item>
<Label />
</ListBox.Item>
</ListBox.Section>
</ListBox>
</ComboBox.Popover>
</ComboBox>
)
<ComponentPreview name="combo-box-with-description" />
<ComponentPreview name="combo-box-with-sections" />
<ComponentPreview name="combo-box-with-disabled-options" />
<ComponentPreview name="combo-box-custom-indicator" />
<ComponentPreview name="combo-box-required" />
<ComponentPreview name="combo-box-custom-value" />
<ComponentPreview name="combo-box-controlled" />
<ComponentPreview name="combo-box-controlled-input-value" />
<ComponentPreview name="combo-box-asynchronous-loading" />
<ComponentPreview name="combo-box-custom-filtering" />
<ComponentPreview name="combo-box-allows-custom-value" />
<ComponentPreview name="combo-box-disabled" />
<ComponentPreview name="combo-box-default-selected-key" />
<ComponentPreview name="combo-box-full-width" />
When used inside a Surface component, use variant="secondary" to apply the lower emphasis variant suitable for surface backgrounds.
<ComponentPreview name="combo-box-on-surface" />
Use the menuTrigger prop to control when the popover opens:
focus (default): popover opens when the user focuses the inputinput: popover opens when the user edits the input textmanual: popover only opens when the user presses the trigger button or uses the arrow keys<ComponentPreview name="combo-box-menu-trigger" />
<ComponentPreview name="combo-box-custom-render-function" />
<RelatedComponents component="combo-box" />import { ComboBox, Input } from '@heroui/react';
function CustomComboBox() {
return (
<ComboBox className="w-full">
<Label>Favorite Animal</Label>
<ComboBox.InputGroup className="border rounded-lg p-2 bg-surface">
<Input placeholder="Search animals..." />
<ComboBox.Trigger className="text-muted" />
</ComboBox.InputGroup>
<ComboBox.Popover>
<ListBox>
<ListBox.Item id="1" textValue="Item 1" className="hover:bg-surface-secondary">
Item 1
</ListBox.Item>
</ListBox>
</ComboBox.Popover>
</ComboBox>
);
}
To customize the ComboBox component classes, you can use the @layer components directive.
@layer components {
.combo-box {
@apply flex flex-col gap-1;
}
.combo-box__input-group {
@apply relative inline-flex items-center;
}
.combo-box__trigger {
@apply absolute right-0 text-muted;
}
.combo-box__popover {
@apply rounded-lg border border-border bg-surface p-2;
}
}
HeroUI follows the BEM methodology to ensure component variants and states are reusable and easy to customize.
The ComboBox component uses these CSS classes (View source styles):
.combo-box - Base ComboBox container.combo-box__input-group - Container for the input and trigger button.combo-box__trigger - The button that triggers the popover.combo-box__popover - The popover container.combo-box[data-invalid="true"] - Invalid state.combo-box[data-disabled="true"] - Disabled ComboBox state.combo-box__trigger[data-focus-visible="true"] - Focused trigger state.combo-box__trigger[data-disabled="true"] - Disabled trigger state.combo-box__trigger[data-open="true"] - Open trigger stateThe component supports both CSS pseudo-classes and data attributes for flexibility:
:hover or [data-hovered="true"] on trigger:focus-visible or [data-focus-visible="true"] on trigger:disabled or [data-disabled="true"] on ComboBox[data-open="true"] on trigger| Prop | Type | Default | Description |
|---|---|---|---|
inputValue | string | - | Current input value (controlled) |
defaultInputValue | string | - | Default input value (uncontrolled) |
onInputChange | (value: string) => void | - | Handler called when the input value changes |
selectedKey | Key | null | - | Current selected key (controlled) |
defaultSelectedKey | Key | null | - | Default selected key (uncontrolled) |
onSelectionChange | (key: Key | null) => void | - | Handler called when the selection changes |
isOpen | boolean | - | Sets the open state of the popover (controlled) |
defaultOpen | boolean | - | Sets the default open state of the popover (uncontrolled) |
onOpenChange | (isOpen: boolean) => void | - | Handler called when the open state changes |
items | Iterable<T> | - | The items to display in the listbox |
disabledKeys | Iterable<Key> | - | Keys of disabled items |
defaultFilter | (text: string, inputValue: string) => boolean | - | Custom filter function for filtering items |
isDisabled | boolean | - | Whether the ComboBox is disabled |
isReadOnly | boolean | - | Whether the input can be selected but not changed by the user |
isRequired | boolean | - | Whether user input is required |
isInvalid | boolean | - | Whether the ComboBox value is invalid |
validate | (value: ComboBoxValidationValue) => ValidationError | true | null | undefined | - | A function that returns an error message if a given value is invalid. Validation errors are displayed to the user when the form is submitted if validationBehavior="native". For realtime validation, use the isInvalid prop instead |
validationBehavior | "native" | "aria" | "native" | Whether to use native HTML form validation to prevent form submission when the value is missing or invalid, or mark the field as required or invalid via ARIA |
name | string | - | The name of the input, used when submitting an HTML form |
form | string | - | The id of a <form> element to associate the input with |
formValue | "text" | "key" | "key" | Whether the text or key of the selected item is submitted as part of an HTML form. When allowsCustomValue is true, this option does not apply and the text is always submitted |
autoComplete | string | - | Describes the type of autocomplete functionality |
autoFocus | boolean | - | Whether the element should receive focus on render |
allowsCustomValue | boolean | - | Whether the ComboBox allows custom values not in the list |
allowsEmptyCollection | boolean | - | Whether the ComboBox allows an empty collection |
menuTrigger | "focus" | "input" | "manual" | "focus" | The interaction required to display the ComboBox menu |
shouldFocusWrap | boolean | - | Whether keyboard navigation is circular |
fullWidth | boolean | false | Whether the ComboBox should take full width of its container |
className | string | - | Additional CSS classes |
children | ReactNode | RenderFunction | - | ComboBox content or render function |
render | DOMRenderFunction<keyof React.JSX.IntrinsicElements, ComboBoxRenderProps> | - | Overrides the default DOM element with a custom render function. |
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | ReactNode | - | InputGroup content |
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | ReactNode | - | Custom trigger content |
| Prop | Type | Default | Description |
|---|---|---|---|
placement | "bottom" | "bottom left" | "bottom right" | "bottom start" | "bottom end" | "top" | "top left" | "top right" | "top start" | "top end" | "left" | "left top" | "left bottom" | "start" | "start top" | "start bottom" | "right" | "right top" | "right bottom" | "end" | "end top" | "end bottom" | "bottom" | Placement of the popover relative to the trigger |
className | string | - | Additional CSS classes |
children | ReactNode | - | Content children |
When using render functions with ComboBox, these values are provided:
| Prop | Type | Description |
|---|---|---|
state | ComboBoxState | The state of the ComboBox |
inputValue | string | The current input value |
selectedKey | Key | null | The currently selected key |
selectedItem | Node | null | The currently selected item |
import { ComboBox, Input, Label, ListBox } from '@heroui/react';
<ComboBox className="w-[256px]">
<Label>Favorite Animal</Label>
<ComboBox.InputGroup>
<Input placeholder="Search animals..." />
<ComboBox.Trigger />
</ComboBox.InputGroup>
<ComboBox.Popover>
<ListBox>
<ListBox.Item id="cat" textValue="Cat">
Cat
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="dog" textValue="Dog">
Dog
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</ComboBox.Popover>
</ComboBox>
import { ComboBox, Input, Label, ListBox, Header, Separator } from '@heroui/react';
<ComboBox className="w-[256px]">
<Label>Country</Label>
<ComboBox.InputGroup>
<Input placeholder="Search countries..." />
<ComboBox.Trigger />
</ComboBox.InputGroup>
<ComboBox.Popover>
<ListBox>
<ListBox.Section>
<Header>North America</Header>
<ListBox.Item id="usa" textValue="United States">
United States
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox.Section>
<Separator />
<ListBox.Section>
<Header>Europe</Header>
<ListBox.Item id="uk" textValue="United Kingdom">
United Kingdom
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox.Section>
</ListBox>
</ComboBox.Popover>
</ComboBox>
import type { Key } from '@heroui/react';
import { ComboBox, Input, Label, ListBox } from '@heroui/react';
import { useState } from 'react';
function ControlledComboBox() {
const [selectedKey, setSelectedKey] = useState<Key | null>('cat');
return (
<ComboBox
className="w-[256px]"
selectedKey={selectedKey}
onSelectionChange={setSelectedKey}
>
<Label>Animal</Label>
<ComboBox.InputGroup>
<Input placeholder="Search animals..." />
<ComboBox.Trigger />
</ComboBox.InputGroup>
<ComboBox.Popover>
<ListBox>
<ListBox.Item id="cat" textValue="Cat">
Cat
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="dog" textValue="Dog">
Dog
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</ComboBox.Popover>
</ComboBox>
);
}
import { ComboBox, Input, Label, ListBox } from '@heroui/react';
import { useState } from 'react';
function ControlledInputComboBox() {
const [inputValue, setInputValue] = useState('');
return (
<ComboBox
className="w-[256px]"
inputValue={inputValue}
onInputChange={setInputValue}
>
<Label>Search</Label>
<ComboBox.InputGroup>
<Input placeholder="Type to search..." />
<ComboBox.Trigger />
</ComboBox.InputGroup>
<ComboBox.Popover>
<ListBox>
<ListBox.Item id="cat" textValue="Cat">
Cat
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="dog" textValue="Dog">
Dog
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</ComboBox.Popover>
</ComboBox>
);
}
import { Collection, ComboBox, EmptyState, Input, Label, ListBox, ListBoxLoadMoreItem, Spinner } from '@heroui/react';
import { useAsyncList } from '@react-stately/data';
interface Character {
name: string;
}
function AsyncComboBox() {
const list = useAsyncList<Character>({
async load({cursor, filterText, signal}) {
const res = await fetch(
cursor || `https://swapi.py4e.com/api/people/?search=${filterText}`,
{ signal }
);
const json = await res.json();
return {
items: json.results,
cursor: json.next,
};
},
});
return (
<ComboBox
allowsEmptyCollection
className="w-[256px]"
inputValue={list.filterText}
onInputChange={list.setFilterText}
>
<Label>Pick a Character</Label>
<ComboBox.InputGroup>
<Input placeholder="Star Wars characters..." />
<ComboBox.Trigger />
</ComboBox.InputGroup>
<ComboBox.Popover>
<ListBox renderEmptyState={() => <EmptyState />}>
<Collection items={list.items}>
{(item) => (
<ListBox.Item id={item.name} textValue={item.name}>
{item.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</Collection>
<ListBoxLoadMoreItem
isLoading={list.loadingState === "loadingMore"}
onLoadMore={list.loadMore}
>
<div className="flex items-center justify-center gap-2 py-2">
<Spinner size="sm" />
<span className="text-sm text-muted">Loading more...</span>
</div>
</ListBoxLoadMoreItem>
</ListBox>
</ComboBox.Popover>
</ComboBox>
);
}
import { ComboBox, Input, Label, ListBox } from '@heroui/react';
<ComboBox
className="w-[256px]"
defaultFilter={(text, inputValue) => {
if (!inputValue) return true;
return text.toLowerCase().includes(inputValue.toLowerCase());
}}
>
<Label>Animal</Label>
<ComboBox.InputGroup>
<Input placeholder="Search animals..." />
<ComboBox.Trigger />
</ComboBox.InputGroup>
<ComboBox.Popover>
<ListBox>
<ListBox.Item id="cat" textValue="Cat">
Cat
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="dog" textValue="Dog">
Dog
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</ComboBox.Popover>
</ComboBox>
Control when the popover opens using the menuTrigger prop:
import { ComboBox, Description, Input, Label, ListBox } from '@heroui/react';
// Opens on focus (default)
<ComboBox className="w-[256px]" menuTrigger="focus">
<Label>Favorite Animal</Label>
<ComboBox.InputGroup>
<Input placeholder="Search animals..." />
<ComboBox.Trigger />
</ComboBox.InputGroup>
<ComboBox.Popover>
<ListBox>
<ListBox.Item id="cat" textValue="Cat">
Cat
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</ComboBox.Popover>
<Description>Popover opens when the input is focused</Description>
</ComboBox>
// Opens when typing
<ComboBox className="w-[256px]" menuTrigger="input">
<Label>Favorite Animal</Label>
<ComboBox.InputGroup>
<Input placeholder="Search animals..." />
<ComboBox.Trigger />
</ComboBox.InputGroup>
<ComboBox.Popover>
<ListBox>
<ListBox.Item id="cat" textValue="Cat">
Cat
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</ComboBox.Popover>
<Description>Popover opens when the user edits the input text</Description>
</ComboBox>
// Opens only manually
<ComboBox className="w-[256px]" menuTrigger="manual">
<Label>Favorite Animal</Label>
<ComboBox.InputGroup>
<Input placeholder="Search animals..." />
<ComboBox.Trigger />
</ComboBox.InputGroup>
<ComboBox.Popover>
<ListBox>
<ListBox.Item id="cat" textValue="Cat">
Cat
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</ComboBox.Popover>
<Description>Popover only opens when the trigger button is pressed or arrow keys are used</Description>
</ComboBox>
Use the formValue prop to control whether the selected item's key or text is submitted in forms:
import { Button, ComboBox, FieldError, Form, Input, Label, ListBox } from '@heroui/react';
function FormValueExample() {
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
console.log('Submitted value:', formData.get('animal')); // Will be "cat" (the key)
};
return (
<Form onSubmit={onSubmit}>
<ComboBox name="animal" formValue="key" isRequired>
<Label>Animal</Label>
<ComboBox.InputGroup>
<Input placeholder="Select an animal..." />
<ComboBox.Trigger />
</ComboBox.InputGroup>
<ComboBox.Popover>
<ListBox>
<ListBox.Item id="cat" textValue="Cat">
Cat
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="dog" textValue="Dog">
Dog
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</ComboBox.Popover>
<FieldError />
</ComboBox>
<ComboBox name="animal-text" formValue="text" isRequired>
<Label>Animal (text)</Label>
<ComboBox.InputGroup>
<Input placeholder="Select an animal..." />
<ComboBox.Trigger />
</ComboBox.InputGroup>
<ComboBox.Popover>
<ListBox>
<ListBox.Item id="cat" textValue="Cat">
Cat
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="dog" textValue="Dog">
Dog
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</ComboBox.Popover>
<FieldError />
</ComboBox>
<Button type="submit">Submit</Button>
</Form>
);
}
Control how validation is displayed using the validationBehavior prop:
import { Button, ComboBox, FieldError, Form, Input, Label, ListBox } from '@heroui/react';
function ValidationExample() {
return (
<div className="space-y-8">
<Form>
<ComboBox name="animal" isRequired validationBehavior="native">
<Label>Animal (native validation)</Label>
<ComboBox.InputGroup>
<Input placeholder="Select an animal..." />
<ComboBox.Trigger />
</ComboBox.InputGroup>
<ComboBox.Popover>
<ListBox>
<ListBox.Item id="cat" textValue="Cat">
Cat
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</ComboBox.Popover>
<FieldError />
</ComboBox>
<Button type="submit">Submit</Button>
</Form>
<Form>
<ComboBox name="animal-aria" isRequired validationBehavior="aria">
<Label>Animal (ARIA validation)</Label>
<ComboBox.InputGroup>
<Input placeholder="Select an animal..." />
<ComboBox.Trigger />
</ComboBox.InputGroup>
<ComboBox.Popover>
<ListBox>
<ListBox.Item id="cat" textValue="Cat">
Cat
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</ComboBox.Popover>
<FieldError />
</ComboBox>
<Button type="submit">Submit</Button>
</Form>
</div>
);
}
Use the validate prop to add custom validation logic:
import { ComboBox, FieldError, Input, Label, ListBox } from '@heroui/react';
function CustomValidationExample() {
return (
<ComboBox
className="w-[256px]"
isRequired
validate={(value) => {
if (!value || value.selectedKey === null) {
return 'Please select an animal';
}
if (value.selectedKey === 'snake') {
return 'Snakes are not allowed';
}
return true;
}}
>
<Label>Favorite Animal</Label>
<ComboBox.InputGroup>
<Input placeholder="Search animals..." />
<ComboBox.Trigger />
</ComboBox.InputGroup>
<ComboBox.Popover>
<ListBox>
<ListBox.Item id="cat" textValue="Cat">
Cat
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="dog" textValue="Dog">
Dog
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="snake" textValue="Snake">
Snake
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</ComboBox.Popover>
<FieldError />
</ComboBox>
);
}
Use the isReadOnly prop to make the comboBox read-only:
import { ComboBox, Input, Label, ListBox } from '@heroui/react';
<ComboBox className="w-[256px]" isReadOnly defaultSelectedKey="cat">
<Label>Favorite Animal</Label>
<ComboBox.InputGroup>
<Input placeholder="Search animals..." />
<ComboBox.Trigger />
</ComboBox.InputGroup>
<ComboBox.Popover>
<ListBox>
<ListBox.Item id="cat" textValue="Cat">
Cat
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="dog" textValue="Dog">
Dog
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</ComboBox.Popover>
</ComboBox>
The ComboBox component implements the ARIA comboBox pattern and provides:
For more information, see the React Aria ComboBox documentation.