Skip to Content
Components
Combobox ALPHA

Combobox

Multi-purpose combobox widget to allow selection from a dynamic set of options.

Documentation

Usage

import { Combobox, ComboboxCheckboxItem, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxScrollArea, ComboboxTrigger, ComboboxValue, } from "@optiaxiom/react/unstable"; import { useState } from "react"; import { colors } from "./data"; export function App() { const [items, setItems] = useState(colors); const [value, setValue] = useState<string[]>([]); return ( <Combobox items={items} onInputValueChange={(inputValue) => { setItems( inputValue ? colors.filter((color) => new RegExp(inputValue, "i").test(color)) : colors, ); }} onItemSelect={(value) => setValue((prev) => prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value], ) } value={value} > <ComboboxTrigger w="224"> <ComboboxValue placeholder="Select colors..." /> </ComboboxTrigger> <ComboboxContent> <ComboboxInput /> <ComboboxScrollArea> {items.map((item) => ( <ComboboxCheckboxItem item={item} key={item}> {item} </ComboboxCheckboxItem> ))} {items.length === 0 && <ComboboxEmpty>No result found</ComboboxEmpty>} </ComboboxScrollArea> </ComboboxContent> </Combobox> ); }

Anatomy

import { Combobox, ComboboxCheckboxItem, ComboboxContent, ComboboxEmpty, ComboboxFooter, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxLabel, ComboboxRadioItem, ComboboxScrollArea, ComboboxSeparator, ComboboxTrigger, ComboboxValue, ComboboxVirtualized, } from "@optiaxiom/react"; export default () => ( <Combobox> <ComboboxTrigger> <ComboboxValue /> </ComboboxTrigger> <ComboboxContent> <ComboboxInput /> <ComboboxScrollArea> <ComboboxItem /> <ComboboxCheckboxItem /> <ComboboxRadioItem /> <ComboboxGroup> <ComboboxLabel /> <ComboboxItem /> <ComboboxCheckboxItem /> <ComboboxRadioItem /> </ComboboxGroup> <ComboboxVirtualized> <ComboboxItem /> <ComboboxCheckboxItem /> <ComboboxRadioItem /> </ComboboxVirtualized> <ComboboxEmpty /> <ComboboxSeparator /> </ComboboxScrollArea> <ComboboxFooter /> </ComboboxContent> </Combobox> );

Data

Use the items prop to pass the collection of data to Combobox. Items can be an array of primitive types such as strings or it can be an array of objects.

In case item is an object you need to pass an additional itemToString prop for Combobox to work.

import { Box } from "@optiaxiom/react"; import { Combobox, ComboboxCheckboxItem, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxScrollArea, ComboboxTrigger, ComboboxValue, } from "@optiaxiom/react/unstable"; import { useState } from "react"; import { type Color, colors } from "./data"; export function App() { const [items, setItems] = useState(colors); const [value, setValue] = useState<Color[]>([]); return ( <Combobox isItemDisabled={(item) => Boolean(item.isDisabled)} items={items} itemToString={(item) => (item ? item.label : "")} onInputValueChange={(inputValue) => { setItems( inputValue ? colors.filter((color) => new RegExp(inputValue, "i").test(color.label), ) : colors, ); }} onItemSelect={(value) => setValue((prev) => prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value], ) } value={value} > <ComboboxTrigger w="224"> <ComboboxValue placeholder="Search a color..." /> </ComboboxTrigger> <ComboboxContent> <ComboboxInput /> <ComboboxScrollArea> {items.map((item) => ( <ComboboxCheckboxItem icon={ <Box rounded="sm" style={{ aspectRatio: 1, backgroundColor: item.color }} /> } item={item} key={item.value} > {item.label} </ComboboxCheckboxItem> ))} {items.length === 0 && <ComboboxEmpty>No result found</ComboboxEmpty>} </ComboboxScrollArea> </ComboboxContent> </Combobox> ); }

Filtering

Use the onInputValueChange handler to handle filtering your list of items as the user is typing.

import { Box } from "@optiaxiom/react"; import { Combobox, ComboboxCheckboxItem, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxScrollArea, ComboboxTrigger, ComboboxValue, } from "@optiaxiom/react/unstable"; import { useState } from "react"; import { type Color, colors } from "./data"; export function App() { const [items, setItems] = useState(colors); const [value, setValue] = useState<Color[]>([]); return ( <Combobox isItemDisabled={(item) => Boolean(item.isDisabled)} items={items} itemToString={(item) => (item ? item.label : "")} onInputValueChange={(inputValue) => { setItems( inputValue ? colors.filter((color) => new RegExp(inputValue, "i").test(color.label), ) : colors, ); }} onItemSelect={(value) => setValue((prev) => prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value], ) } value={value} > <ComboboxTrigger w="224"> <ComboboxValue placeholder="Search a color..." /> </ComboboxTrigger> <ComboboxContent> <ComboboxInput /> <ComboboxScrollArea> {items.map((item) => ( <ComboboxCheckboxItem icon={ <Box rounded="sm" style={{ aspectRatio: 1, backgroundColor: item.color }} /> } item={item} key={item.value} > {item.label} </ComboboxCheckboxItem> ))} {items.length === 0 && <ComboboxEmpty>No result found</ComboboxEmpty>} </ComboboxScrollArea> </ComboboxContent> </Combobox> ); }

Controlled

We can only use Combobox in a controlled mode and using the following props:

  • value: Represents an array of currently selected elements from the items array.
  • onItemSelect: Handler invoked when user selects an item with the item as the only parameter.

Developers are expected to manually track the selection on their side since items inside a combobox can represent values or actions.

import { Combobox, ComboboxCheckboxItem, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxScrollArea, ComboboxTrigger, ComboboxValue, } from "@optiaxiom/react/unstable"; import { useState } from "react"; import { colors } from "./data"; import { useSet } from "./useSet"; export function App() { const [items, setItems] = useState(colors); const [value, { toggle }] = useSet([colors[1]]); return ( <Combobox items={items} itemToString={(item) => item?.label || ""} onInputValueChange={(inputValue) => { setItems( inputValue ? colors.filter((color) => new RegExp(inputValue, "i").test(color.label), ) : colors, ); }} onItemSelect={(value) => toggle(value)} value={value} > <ComboboxTrigger w="224"> <ComboboxValue placeholder="Search a color..." /> </ComboboxTrigger> <ComboboxContent> <ComboboxInput /> <ComboboxScrollArea> {items.map((item) => ( <ComboboxCheckboxItem item={item} key={item.label}> {item.label} </ComboboxCheckboxItem> ))} {items.length === 0 && <ComboboxEmpty>No result found</ComboboxEmpty>} </ComboboxScrollArea> </ComboboxContent> </Combobox> ); }

Disabled

Use the optional isItemDisabled prop to configure which items should be disabled. The callback is passed the item and the index number as arguments and expects a boolean result.

import { Box } from "@optiaxiom/react"; import { Combobox, ComboboxCheckboxItem, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxScrollArea, ComboboxTrigger, ComboboxValue, } from "@optiaxiom/react/unstable"; import { useState } from "react"; import { type Color, colors } from "./data"; export function App() { const [items, setItems] = useState(colors); const [value, setValue] = useState<Color[]>([]); return ( <Combobox isItemDisabled={(item) => Boolean(item.isDisabled)} items={items} itemToString={(item) => (item ? item.label : "")} onInputValueChange={(inputValue) => { setItems( inputValue ? colors.filter((color) => new RegExp(inputValue, "i").test(color.label), ) : colors, ); }} onItemSelect={(value) => setValue((prev) => prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value], ) } value={value} > <ComboboxTrigger w="224"> <ComboboxValue placeholder="Search a color..." /> </ComboboxTrigger> <ComboboxContent> <ComboboxInput /> <ComboboxScrollArea> {items.map((item) => ( <ComboboxCheckboxItem icon={ <Box rounded="sm" style={{ aspectRatio: 1, backgroundColor: item.color }} /> } item={item} key={item.value} > {item.label} </ComboboxCheckboxItem> ))} {items.length === 0 && <ComboboxEmpty>No result found</ComboboxEmpty>} </ComboboxScrollArea> </ComboboxContent> </Combobox> ); }

Creatable

We can combine filtering and controlled usage to build creatable comboboxes by allowing the user to add new entries on the fly.

import { Combobox, ComboboxCheckboxItem, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxScrollArea, ComboboxTrigger, ComboboxValue, } from "@optiaxiom/react/unstable"; import { useState } from "react"; import { type Color, colors } from "./data"; import { useSet } from "./useSet"; export function App() { const [items, setItems] = useState(colors); const [value, { toggle }] = useSet<Color>([]); const [inputValue, setInputValue] = useState(""); const filteredItems = inputValue ? [ ...items.filter((color) => new RegExp(inputValue, "i").test(color.label), ), ...(items.find( (color) => color.label.toLowerCase() === inputValue.toLowerCase(), ) ? [] : [{ label: inputValue, new: true }]), ] : items; return ( <Combobox inputValue={inputValue} items={filteredItems} itemToString={(item) => item?.label || ""} onInputValueChange={setInputValue} onItemSelect={(value) => { if (value.new) { const newItem = { label: value.label }; setItems((items) => [...items, newItem]); toggle(newItem); setInputValue(""); } else { toggle(value); } }} value={value} > <ComboboxTrigger w="224"> <ComboboxValue placeholder="Search a color..." /> </ComboboxTrigger> <ComboboxContent> <ComboboxInput /> <ComboboxScrollArea> {filteredItems.map((item) => ( <ComboboxCheckboxItem item={item} key={item.label}> {item.new ? `Create "${item.label}"` : item.label} </ComboboxCheckboxItem> ))} {filteredItems.length === 0 && ( <ComboboxEmpty>No result found</ComboboxEmpty> )} </ComboboxScrollArea> </ComboboxContent> </Combobox> ); }

Trigger

By default we use the Button component for the combobox trigger which accepts all of the existing button props.

Item

The ComboboxCheckboxItem component accepts icon, addonBefore, addonAfter, and description props for additional content inside the items.

import { Box } from "@optiaxiom/react"; import { Combobox, ComboboxCheckboxItem, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxScrollArea, ComboboxTrigger, ComboboxValue, } from "@optiaxiom/react/unstable"; import { useState } from "react"; import { type Color, colors } from "./data"; export function App() { const [items, setItems] = useState(colors); const [value, setValue] = useState<Color[]>([]); return ( <Combobox isItemDisabled={(item) => Boolean(item.isDisabled)} items={items} itemToString={(item) => (item ? item.label : "")} onInputValueChange={(inputValue) => { setItems( inputValue ? colors.filter((color) => new RegExp(inputValue, "i").test(color.label), ) : colors, ); }} onItemSelect={(value) => setValue((prev) => prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value], ) } value={value} > <ComboboxTrigger w="224"> <ComboboxValue placeholder="Search a color..." /> </ComboboxTrigger> <ComboboxContent> <ComboboxInput /> <ComboboxScrollArea> {items.map((item) => ( <ComboboxCheckboxItem icon={ <Box rounded="sm" style={{ aspectRatio: 1, backgroundColor: item.color }} /> } item={item} key={item.value} > {item.label} </ComboboxCheckboxItem> ))} {items.length === 0 && <ComboboxEmpty>No result found</ComboboxEmpty>} </ComboboxScrollArea> </ComboboxContent> </Combobox> ); }

Empty

Use the ComboboxEmpty component when there are no available items or suggestions.

import { Box } from "@optiaxiom/react"; import { Combobox, ComboboxCheckboxItem, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxScrollArea, ComboboxTrigger, ComboboxValue, } from "@optiaxiom/react/unstable"; import { useState } from "react"; import { type Color, colors } from "./data"; export function App() { const [items, setItems] = useState(colors); const [value, setValue] = useState<Color[]>([]); return ( <Combobox isItemDisabled={(item) => Boolean(item.isDisabled)} items={items} itemToString={(item) => (item ? item.label : "")} onInputValueChange={(inputValue) => { setItems( inputValue ? colors.filter((color) => new RegExp(inputValue, "i").test(color.label), ) : colors, ); }} onItemSelect={(value) => setValue((prev) => prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value], ) } value={value} > <ComboboxTrigger w="224"> <ComboboxValue placeholder="Search a color..." /> </ComboboxTrigger> <ComboboxContent> <ComboboxInput /> <ComboboxScrollArea> {items.map((item) => ( <ComboboxCheckboxItem icon={ <Box rounded="sm" style={{ aspectRatio: 1, backgroundColor: item.color }} /> } item={item} key={item.value} > {item.label} </ComboboxCheckboxItem> ))} {items.length === 0 && <ComboboxEmpty>No result found</ComboboxEmpty>} </ComboboxScrollArea> </ComboboxContent> </Combobox> ); }

Loading

Use the loading prop on ComboboxScrollArea to show a loading spinner if the initial data for the combobox has not loaded yet.

import { Combobox, ComboboxCheckboxItem, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxScrollArea, ComboboxTrigger, ComboboxValue, } from "@optiaxiom/react/unstable"; import { useEffect, useState } from "react"; import { colors } from "./data"; export function App() { const [items, setItems] = useState<string[]>([]); const [inputValue, setInputValue] = useState(""); const filteredItems = inputValue ? items.filter((color) => new RegExp(inputValue, "i").test(color)) : items; const [open, setOpen] = useState(false); useEffect(() => { const timer = setTimeout(() => setItems(open ? colors : []), 3000); return () => clearTimeout(timer); }, [open]); return ( <Combobox items={filteredItems} onInputValueChange={setInputValue} onOpenChange={setOpen} open={open} > <ComboboxTrigger w="224"> <ComboboxValue placeholder="Search a color..." /> </ComboboxTrigger> <ComboboxContent> <ComboboxInput /> <ComboboxScrollArea loading={items.length === 0}> {filteredItems.map((item) => ( <ComboboxCheckboxItem item={item} key={item}> {item} </ComboboxCheckboxItem> ))} {filteredItems.length === 0 && ( <ComboboxEmpty>No result found</ComboboxEmpty> )} </ComboboxScrollArea> </ComboboxContent> </Combobox> ); }

Virtualized

We can use ComboboxVirtualized component to improve performance when rendering a large number of items inside the combobox.

import { Combobox, ComboboxCheckboxItem, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxScrollArea, ComboboxTrigger, ComboboxValue, ComboboxVirtualized, } from "@optiaxiom/react/unstable"; import { useState } from "react"; const colors = Array.from({ length: 2000 }).map( (_, index) => `Color ${index + 1}`, ); export function App() { const [items, setItems] = useState(colors); const [value, setValue] = useState([colors[0]]); return ( <Combobox items={items} onInputValueChange={(inputValue) => { setItems( inputValue ? colors.filter((color) => new RegExp(inputValue, "i").test(color)) : colors, ); }} onItemSelect={(value) => setValue((prev) => prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value], ) } value={value} > <ComboboxTrigger w="224"> <ComboboxValue placeholder="Search a color..." /> </ComboboxTrigger> <ComboboxContent> <ComboboxInput /> <ComboboxScrollArea> <ComboboxVirtualized items={items}> {(item) => ( <ComboboxCheckboxItem item={item} key={item}> {item} </ComboboxCheckboxItem> )} </ComboboxVirtualized> {items.length === 0 && <ComboboxEmpty>No result found</ComboboxEmpty>} </ComboboxScrollArea> </ComboboxContent> </Combobox> ); }

Related

DropdownMenu

Display a dropdown menu.

Select

Single select combobox widget to allow selection from a fixed set of options.

Props

Combobox

Doesn't render its own HTML element.

Prop

defaultOpen

The initial open state in uncontrolled mode.

false | true

Default: false

inputValue

The input value in controlled mode.

string

isItemDisabled

Return true if items need to be marked as disabled and skipped from keyboard navigation.

(item: Item, index: number) => boolean

Default: () => false

items*

The items we want to render.

Item[]

itemToKey

Return a unique key for each item when the object reference will change during renders.

(item: Item) => any

Default: (value) => value

itemToString

Return a string representation of items if they are objects. Needed to show selected values inside triggers.

(item: Item) => string

Default: (value) => (value ? String(value) : "")

onInputValueChange

Handler that is called when input value changes.

(inputValue: string) => void

onItemSelect

Handler that is called when an item is selected either via keyboard or mouse.

(value: Item) => void

onOpenChange

Handler that is called when the open state changes.

(open: boolean) => void

open

The open state in controlled mode.

false | true

value

Item[] | Set<Item>

ComboboxTrigger

Supports all Button props in addition to its own. Renders a <button> element.

Prop

addonAfter

Display content inside the button after children.

ReactNode

addonBefore

Display content inside the button before children.

ReactNode

appearance

Control the appearance by selecting between the different button types.

"default" | "danger" | "primary" | "subtle" | "danger-outline" | "inverse"

asChild

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

false | true

className

string

disabled

Whether the button is disabled.

false | true

hasCustomAnchor

false | true

icon

Display an icon before or after the button content or omit children to only show the icon.

ReactNode

iconPosition

Control whether to show the icon before or after the button content.

"end" | "start"

loading

Whether to show loading spinner inside the button.

false | true

size

Control the size of the button.

"sm" | "md" | "lg"

square

Whether button should have square shape.

false | true

ComboboxValue

Supports all Box props in addition to its own. Renders a <div> element.

Prop

asChild

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

false | true

className

string

placeholder

string

ComboboxContent

Supports all Box props in addition to its own. Renders a <div> element.

Prop

align

"center" | "end" | "start"

asChild

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

false | true

className

string

minW

Whether to set the min-width to the width of the trigger.

"0" | "trigger"

onCloseAutoFocus

Event handler called when auto-focusing on close. Can be prevented.

(event: Event) => void

onEscapeKeyDown

Event handler called when the escape key is down. Can be prevented.

(event: KeyboardEvent) => void

onFocusOutside

Event handler called when the focus moves outside of the DismissableLayer. Can be prevented.

(event: FocusOutsideEvent) => void

onInteractOutside

Event handler called when an interaction happens outside the DismissableLayer. Specifically, when a pointerdown event happens outside or focus moves outside of it. Can be prevented.

(event: FocusOutsideEvent | PointerDownOutsideEvent) => void

onOpenAutoFocus

Event handler called when auto-focusing on open. Can be prevented.

(event: Event) => void

onPointerDownOutside

Event handler called when the a pointerdown event happens outside of the DismissableLayer. Can be prevented.

(event: PointerDownOutsideEvent) => void

side

"bottom" | "left" | "right" | "top"

sideOffset

number

transitionType

"pop" | "fade" | "slide" | "slidePop"

withArrow

Whether to show an arrow.

false | true

ComboboxInput

Supports all Input props in addition to its own. Renders a <div> element but forwards all props to an inner <input> element.

Prop

addonAfter

Display content inside the input at the end.

ReactNode

addonBefore

Display content inside the input at the start.

ReactNode

addonPointerEvents

When this prop is set to none clicking empty space inside the addon will focus the input box.

"none" | "auto"

appearance

Control the appearance of the input.

"number" | "default"

asChild

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

false | true

className

string

disabled

Whether the input is disabled.

false | true

error

Whether to show the input error state.

false | true

htmlSize

Control the native input size attribute.

number

size

Control the size of the input.

"md" | "lg" | "xl"

ComboboxScrollArea

Supports all Box props in addition to its own. Renders a <div> element.

Prop

asChild

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

false | true

className

string

loading

Whether to show loading spinner inside the menu.

false | true

ComboboxVirtualized

Supports all Box props in addition to its own. Renders a <div> element.

Prop

asChild

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

false | true

className

string

items*

T[]

ComboboxItem

Supports all Box props in addition to its own. Renders a <div> element.

Prop

asChild

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

false | true

className

string

item*

The exact item object from the collection.

unknown

selected

Whether to override the default selected state.

false | true

ComboboxCheckboxItem

Supports all Box props in addition to its own. Renders a <div> element.

Prop

addonAfter

Display content inside the item after children.

ReactNode

addonBefore

Display content inside the item before children.

ReactNode

asChild

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

false | true

className

string

description

Add secondary text after the primary label.

ReactNode

icon

Display an icon before the item content.

ReactNode

intent

Control the appearance by selecting between the different item types.

"danger" | "neutral"

item*

The exact item object from the collection.

unknown

selected

Whether to override the default selected state.

false | true

ComboboxRadioItem

Supports all Box props in addition to its own. Renders a <div> element.

Prop

addonAfter

Display content inside the item after children.

ReactNode

addonBefore

Display content inside the item before children.

ReactNode

asChild

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

false | true

className

string

description

Add secondary text after the primary label.

ReactNode

icon

Display an icon before the item content.

ReactNode

intent

Control the appearance by selecting between the different item types.

"danger" | "neutral"

item*

The exact item object from the collection.

unknown

selected

Whether to override the default selected state.

false | true

ComboboxGroup

Supports all Box props in addition to its own. Renders a <div> element.

Prop

asChild

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

false | true

className

string

ComboboxLabel

Supports all Box props in addition to its own. Renders a <div> element.

Prop

asChild

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

false | true

className

string

ComboboxSeparator

Supports all Separator props in addition to its own. Renders a <div> element.

Prop

asChild

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

false | true

className

string

decorative

Whether or not the component is purely decorative. When true, accessibility-related attributes are updated so that that the rendered element is removed from the accessibility tree.

false | true

orientation

ResponsiveValue<"horizontal" | "vertical">

ComboboxEmpty

Supports all Box props in addition to its own. Renders a <div> element.

Prop

asChild

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

false | true

className

string

ComboboxFooter

Supports all Box props in addition to its own. Renders a <div> element.

Prop

asChild

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read the Composition guide for more details.

false | true

className

string

Changelog

0.4.0

  • Added component
Last updated on