Skip to Content
ComponentsCombobox

Combobox

A styled autocomplete/combobox built on Base UI’s Combobox primitive. Includes text input with built-in filtering, popup with items, grouped options, multi-select with chips, and built-in Floating UI positioning.

Import

import { Combobox } from "chunks-ui";

Basic Usage

const fruits = [ { value: "apple", label: "Apple" }, { value: "banana", label: "Banana" }, { value: "cherry", label: "Cherry" }, ]; <Combobox.Root items={fruits}> <div className="relative"> <Combobox.Input placeholder="Search a fruit..." aria-label="Fruit" /> <Combobox.Trigger /> </div> <Combobox.Portal> <Combobox.Positioner> <Combobox.Popup> <Combobox.List> {(item) => ( <Combobox.Item key={item.value} value={item}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> )} </Combobox.List> <Combobox.Empty>No results found.</Combobox.Empty> </Combobox.Popup> </Combobox.Positioner> </Combobox.Portal> </Combobox.Root>

Combobox.Empty shows when no item matches the input. Set items (or filteredItems) on Combobox.Root, or it always shows.

Pass each item object as the Combobox.Item value. Base UI shows an object’s label in the input and submits its value with a form, so picking “Banana” fills the input with “Banana”. For other object shapes, set itemToStringLabel (and itemToStringValue for forms) on Combobox.Root. If you pass the selected value in from elsewhere (value/defaultValue) and it isn’t the same object as the item, set isItemEqualToValue.

With Clear Button

<Combobox.Root items={fruits}> <div className="relative"> <Combobox.Input placeholder="Search a fruit..." aria-label="Fruit" /> <Combobox.Clear /> <Combobox.Trigger /> </div> <Combobox.Portal> <Combobox.Positioner> <Combobox.Popup> <Combobox.List> {(item) => ( <Combobox.Item key={item.value} value={item}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> )} </Combobox.List> <Combobox.Empty>No results found.</Combobox.Empty> </Combobox.Popup> </Combobox.Positioner> </Combobox.Portal> </Combobox.Root>

Grouped Options

Pass groups ({ value, items }) as items. Combobox.List then calls its child once per group, with only the group’s matching items, and hides empty groups while filtering:

const colors = [ { value: "Warm", items: [ { value: "red", label: "Red" }, { value: "orange", label: "Orange" }, { value: "yellow", label: "Yellow" }, ], }, { value: "Cool", items: [ { value: "blue", label: "Blue" }, { value: "green", label: "Green" }, { value: "purple", label: "Purple" }, ], }, ]; <Combobox.Root items={colors}> <div className="relative"> <Combobox.Input placeholder="Pick a color..." aria-label="Color" /> <Combobox.Trigger /> </div> <Combobox.Portal> <Combobox.Positioner> <Combobox.Popup> <Combobox.List> {(group: (typeof colors)[number]) => ( <Combobox.Group key={group.value} items={group.items}> <Combobox.GroupLabel>{group.value}</Combobox.GroupLabel> {group.items.map((item) => ( <Combobox.Item key={item.value} value={item}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> ))} </Combobox.Group> )} </Combobox.List> <Combobox.Empty>No results found.</Combobox.Empty> </Combobox.Popup> </Combobox.Positioner> </Combobox.Portal> </Combobox.Root>

Multi-Select with Chips

type Framework = { value: string; label: string }; function MultiSelectCombobox() { const [value, setValue] = useState<Framework[]>([]); return ( <Combobox.Root multiple items={frameworks} value={value} onValueChange={setValue}> <Combobox.Control> <Combobox.Chips> {value.map((item) => ( <Combobox.Chip key={item.value}> {item.label} <Combobox.ChipRemove /> </Combobox.Chip> ))} <Combobox.Input className="h-7 min-w-20 flex-1 border-0 bg-transparent px-0" placeholder="Select frameworks..." aria-label="Frameworks" /> </Combobox.Chips> <Combobox.Clear /> <Combobox.Trigger /> </Combobox.Control> <Combobox.Portal> <Combobox.Positioner> <Combobox.Popup> <Combobox.List> {(item) => ( <Combobox.Item key={item.value} value={item}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> )} </Combobox.List> <Combobox.Empty>No results found.</Combobox.Empty> </Combobox.Popup> </Combobox.Positioner> </Combobox.Portal> </Combobox.Root> ); }

Put Combobox.Input inside Combobox.Chips, after the chips. Only there does the keyboard reach the chips: ArrowLeft (ArrowRight in a right-to-left layout) at the start of the input focuses the last chip, and Backspace in an empty input removes it.

A select with a search field: Combobox.Trigger is a button that shows Combobox.Value, and Combobox.Input sits inside the popup.

<Combobox.Root items={fruits}> <Combobox.Trigger aria-label="Favorite fruit" className="static h-ui-height w-full rounded-md border border-input bg-background px-3 text-sm max-md:text-base md:[@media(pointer:coarse)]:text-base focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring" > <Combobox.Value placeholder="Pick a fruit" /> <Combobox.Icon /> </Combobox.Trigger> <Combobox.Portal> <Combobox.Positioner> <Combobox.Popup aria-label="Fruits" className="flex flex-col overflow-hidden"> <Combobox.Input placeholder="Search..." aria-label="Search fruits" className="mb-1 shrink-0 pe-3" /> <Combobox.List className="min-h-0 overflow-y-auto overscroll-contain scroll-py-1"> {(item) => ( <Combobox.Item key={item.value} value={item}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> )} </Combobox.List> <Combobox.Empty>No results found.</Combobox.Empty> </Combobox.Popup> </Combobox.Positioner> </Combobox.Portal> </Combobox.Root>

Combobox.Trigger’s default classes put a chevron over the end of an input: absolute inset-y-0 end-0 flex items-center pe-2. cn merges your className over them, so static replaces absolute (which leaves inset-y-0 end-0 without effect) and px-3 replaces pe-2. The other classes draw the field, its text size and its focus ring as on Select.Trigger.

In the popup, pe-3 replaces the end padding (pe-12) that Combobox.Input keeps for the buttons over its end. Combobox.Popup scrolls as a whole, so in a long list the search input would scroll out of view: flex flex-col overflow-hidden stops the popup scrolling, shrink-0 keeps the input’s height, and min-h-0 overflow-y-auto makes Combobox.List scroll below the input instead.

With children, Combobox.Trigger loses its default name, “Show options”, and the value it shows does not name it either: with the input inside the popup, Base UI gives the trigger the combobox role, which takes no name from its content. Pass an aria-label, as here, or put the Combobox in Field.Root with a Field.Label. With the input inside, the popup is a dialog, so name it too, with an aria-label on Combobox.Popup.

Controlled

type Status = { value: string; label: string }; const statuses: Status[] = [ { value: "active", label: "Active" }, { value: "inactive", label: "Inactive" }, { value: "pending", label: "Pending" }, ]; function ControlledCombobox() { const [value, setValue] = useState<Status | null>(null); return ( <Combobox.Root items={statuses} value={value} onValueChange={setValue}> <div className="relative"> <Combobox.Input placeholder="Select status..." aria-label="Status" /> <Combobox.Clear /> <Combobox.Trigger /> </div> <Combobox.Portal> <Combobox.Positioner> <Combobox.Popup> <Combobox.List> {(item) => ( <Combobox.Item key={item.value} value={item}> {item.label} <Combobox.ItemIndicator /> </Combobox.Item> )} </Combobox.List> <Combobox.Empty>No results found.</Combobox.Empty> </Combobox.Popup> </Combobox.Positioner> </Combobox.Portal> </Combobox.Root> ); }

Animation

The popup enters with a scale-up and fade (from scale-95 and opacity-0): the popup spring (springs.popup) with Motion installed, a CSS transition of opacity and scale without it; reduced motion turns off both. The popup grows out of its trigger (origin-(--transform-origin)); a className such as origin-top replaces it.

render on Combobox.Popup replaces the element the spring animates, so a popup with render uses the CSS transition even with Motion installed. To restyle the popup, pass className.

Compound Parts

PartDescription
Combobox.RootState container. Manages open/close, filtering, and selected value(s).
Combobox.ControlBordered wrapper for multi-select (chips + input): Base UI’s Combobox.InputGroup, a role="group" (pass aria-label to name it). The popup anchors to it, and a press on its padding focuses the input and opens the popup, unless openOnInputClick is false. Combobox.Clear and Combobox.Trigger sit over its end: its end padding (pe-12) keeps the chips and the input’s text out from under them. A px-* or p-* class, with or without a variant such as md:, replaces it: add pe-12 back. Draws the focus ring (has-[:focus-visible]) while the input or a chip inside it has keyboard focus, and turns off the ring of its combobox input, so one ring shows; a focused chip also draws its own.
Combobox.InputText input with built-in filtering. In a single-select, Combobox.Clear and Combobox.Trigger sit over its end: its end padding (pe-12) keeps the text out from under them. A px-* or p-* class, with or without a variant such as md:, replaces it: add pe-12 back. Inside Combobox.Chips, the example’s px-0 removes it, and the Control’s end padding keeps the text clear instead. A placeholder cut at the end of the text box ends in an ellipsis (placeholder-shown:text-ellipsis), except while the input has focus in Chromium browsers and Safari, which cut it; a cut value has no ellipsis. placeholder-shown:text-clip in className removes the ellipsis; a plain text-clip does not, because the :placeholder-shown rule is more specific.
Combobox.TriggerButton that toggles the popup. Renders a default chevron icon, named “Show options”. Children or a render element replace both the chevron and the name: see Accessible Names below. At most as wide as its container (max-w-full), and it shrinks in a flex row (min-w-0); max-w-none lifts the cap.
Combobox.IconChevron icon, usable standalone outside the trigger. Children you pass replace the chevron. With a render element and no children, there is no chevron: the element draws its own, and may be a void element such as <img>.
Combobox.PortalRenders the popup in a portal.
Combobox.PositionerHandles Floating UI positioning. Default sideOffset={8}, as on Popover.Content and Menu.Content: the input’s focus ring, 4px outside its edge, stays clear of the popup. A sideOffset you pass replaces it, so sideOffset={0} opens the popup flush against the input, or against the trigger when the input is inside the popup. Inside a Combobox.Control the popup anchors to the Control: it is as wide as the Control and opens 8px below the Control’s border.
Combobox.PopupThe dropdown panel containing items. It is as wide as the input, as the Combobox.Control in a multi-select, or as the trigger when Combobox.Input is inside the popup. Item text wraps to that width, and a word longer than the popup breaks. Scrolls when the items need more than the space between the input (or that trigger) and the viewport edge.
Combobox.ListList container. Takes a function child that renders each matching item (or each group, when items holds groups).
Combobox.ItemAn individual selectable option. Its text wraps, and a word longer than the popup breaks; give an icon, badge or shortcut beside the text shrink-0, or it is squeezed.
Combobox.ItemIndicatorCheckmark shown for the selected item. Children you pass replace the checkmark. With a render element and no children, there is no checkmark: the element draws its own, and may be a void element such as <img>. The indicator is aria-hidden, so screen readers skip its children and announce the option’s selected state instead.
Combobox.EmptyMessage displayed when no items match the filter. Needs items on Root.
Combobox.ClearButton that clears the current value. Renders a default X icon. Children you pass replace the X; with a render element and no children, there is no X. The button keeps its name, “Clear selection”, whatever its content: if the children are visible text, pass an aria-label that contains it.
Combobox.GroupGroups related items together.
Combobox.GroupLabelLabel for a group of items.
Combobox.ChipsContainer for selected chips in multi-select mode: a wrapping row with a 4px gap between the chips (gap-1; className merges with it). Takes ordinary children: map the selected values to Combobox.Chips, and put Combobox.Input after them. It fills the free width of Combobox.Control (flex-1), so an input with flex-1, as in the example, takes the rest of the row; flex-initial in className sizes the row by its content instead.
Combobox.ChipA styled chip representing a selected value. Draws the focus ring when the arrow keys focus it. Arrow keys move focus from the input to a chip only when the input is inside Combobox.Chips.
Combobox.ChipRemoveButton to remove a chip. Renders a default X icon. Children you pass replace the X; with a render element and no children, there is no X. The button keeps its name, “Remove” and the chip’s text, whatever its icon. Inside a chip, text children are read with the chip’s text (“Remove Apple Drop”), so pass an aria-label with text children.
Combobox.ValueDisplays the selected value, or placeholder while nothing is selected, in a <span> that stays on one line and ends in an ellipsis when the value is wider than its trigger, and fills the trigger’s free space (flex-1, so an element beside it keeps its width); flex-initial in className sizes it by its text instead, and whitespace-normal brings back wrapping. In a right-to-left trigger, a long Latin value shows its last words: the line starts at the right edge, so its first words overflow on the left. dir="auto" on the value takes the direction from its first letter: a Latin value then shows its first words and sits at the left, and a right-to-left value stays at the right. While nothing is selected, what the <span> shows is muted (text-muted-foreground), as in Select: the placeholder, static children, and a function child’s result in a single-select. To colour it yourself, use in-data-placeholder:text-*; in-data-placeholder:text-inherit keeps it in the colour around it. A plain text-* in className colours the value only: the muted colour stays beside it and wins while nothing is selected. A function child in a multi-select gets no <span>, so it can return the chips or the Combobox.Chips itself, but text it returns is neither clipped nor muted: return your own clipped element, such as <span className="flex-1 truncate text-start">...</span>.
Combobox.StatusAccessible status announcements for screen readers.

Accessible Names

Combobox.Trigger, Combobox.Clear and Combobox.ChipRemove show only an icon, so each one has a default name: “Show options”, “Clear selection” and “Remove” followed by the text of its chip (“Remove Apple”). Pass labels to Combobox.Root to translate them, or aria-label to a part to use a different name:

<Combobox.ChipRemove aria-label={`Deselect ${item.label}`} />

Combobox.Clear and Combobox.ChipRemove keep their name whatever you pass. children replace the X, and a render element with no children gets no X. If the children are visible text, pass an aria-label that contains it, so speech input users can say what they see.

Combobox.Trigger is different: it can hold content, such as the selected value. If you pass children or a render element, its default name and its chevron are not applied. Beside an input, the content you pass names the button:

<Combobox.Trigger render={<button type="button">Choose</button>} />

Give icon-only content an aria-label.

With the input inside the popup, as in Trigger with Search, the trigger is a combobox, which takes no name from its content: give it an aria-label or a Field.Label.

Inside Field.Root, Base UI sets aria-labelledby on Combobox.Trigger to the field label. That label takes precedence over aria-label, so the default “Show options” name does not apply there.

When Combobox.Input is outside the popup, Base UI hides everything but the popup, the input and elements with aria-live from assistive technology (aria-hidden) while the popup is open. That includes Combobox.Clear, Combobox.Trigger and, in a multi-select, the chips and their Combobox.ChipRemove buttons, so a screen reader user can reach them only after the popup closes.

Touch Targets

Combobox.Clear is 14px wide and Combobox.Trigger is 24px wide. To make the clear button easier to tap, add touch-target to it: its hit area grows to 44×44px, and the layout stays the same. The area also covers the last 15px of the input before the button. Keep Combobox.Clear before Combobox.Trigger in the DOM, as in the examples: the clear button’s area covers part of the trigger, and DOM order makes the trigger paint above it.

<div className="relative"> <Combobox.Input placeholder="Search a fruit..." aria-label="Fruit" /> <Combobox.Clear className="touch-target" /> <Combobox.Trigger /> </div>

Add touch-target to Combobox.Trigger only when there is no Combobox.Clear next to it. Their centres are 23px apart, so the trigger’s area would cover the end of the clear button, and a press there would open the popup.

Text Size

The text of Combobox.Input is 16px below the md breakpoint (768px by default) and on a touch screen, so iOS Safari does not zoom the page when the field takes focus (it zooms into text under 16px); otherwise it is 14px, as in Input. With a mouse, a text-size class on Combobox.Input, such as text-lg, sets the size from md up. A max-md: class, such as max-md:text-lg, sets it below md, on a touch screen too; from md up on a touch screen, a md:[@media(pointer:coarse)]: class sets it. See Text Size in Input for the limits.

Props

All compound parts accept the same props as their underlying Base UI Combobox counterparts. Combobox.Control is Base UI’s Combobox.InputGroup and accepts its props. Combobox.Value also takes a <span>’s props. Combobox.Root also takes labels:

NameTypeDefault
labelsPartial<ComboboxLabels>

Replaces the built-in English strings, e.g. to translate them. Keys you leave out keep the default from DEFAULT_COMBOBOX_LABELS. An aria-label on a part takes precedence.

Labels

labels replaces the built-in English text, for example to translate it. Keys you leave out keep their default from DEFAULT_COMBOBOX_LABELS. An aria-label on a part takes precedence. See Labels.

NameTypeDefault
triggerstring

Name of Combobox.Trigger when it renders the default icon.

"Show options"
clearstring

Name of Combobox.Clear, whatever its content.

"Clear selection"
chipRemovestring

Name of Combobox.ChipRemove, read with its chip’s text. {chip} marks where the text goes: "{chip} を削除" reads “Apple を削除”. Without {chip}, the text follows the label, so the default reads “Remove Apple”. Only the first {chip} counts.

"Remove"
Last updated on