Combobox An autocomplete input component that allows users to select from a filterable list of options with support for single and multi-select.
The Combobox component provides an accessible autocomplete input that combines a text input with a filterable dropdown list. It supports both single and multi-select modes, grouped options, custom rendering, and creatable items. Built on Base UI primitives, it offers full keyboard navigation and accessibility features.
import {
Combobox,
ComboboxChips,
ComboboxClear,
ComboboxContent,
ComboboxIcon,
ComboboxInput,
ComboboxItem,
ComboboxItemIndicator,
ComboboxList,
ComboboxValue,
ComboboxWrapper,
} from "@tilt-legal/cubitt-components/primitives" ;
// Basic single select
< Combobox items = {options}>
< ComboboxWrapper >
< ComboboxInput placeholder = "Select an option..." />
< ComboboxClear />
< ComboboxIcon />
</ ComboboxWrapper >
< ComboboxContent >
< ComboboxEmpty >No results found.</ ComboboxEmpty >
< ComboboxList >
{( item ) => (
< ComboboxItem value = {item}>
< ComboboxItemIndicator />
{item.label}
</ ComboboxItem >
)}
</ ComboboxList >
</ ComboboxContent >
</ Combobox >
URL State ManagementUsing Cubitt's URL-state hooks, you can sync the combobox selection with the URL by providing a paramName. The component automatically detects common ID fields (id, value, code, key) from objects to generate clean URLs:
// Single select - synced with ?category=electronics
< Combobox
items = {categories}
paramName = "category"
defaultValue = "all"
>
{ /* ... */ }
</ Combobox >
// Multi-select - synced with ?tags=react,typescript,ui
< Combobox
items = {tags}
multiple
paramName = "tags"
defaultValue = {[]}
>
{ /* ... */ }
</ Combobox >
// Advanced options:
< Combobox
items = {options}
paramName = "filter"
paramClearOnDefault = { true } // Remove param when value equals default
paramDebounce = { 300 } // Debounce URL updates
onUrlValueChange = {( value ) => console. log ( "Selected:" , value)}
>
{ /* ... */ }
</ Combobox > For objects with non-standard ID fields, provide itemToStringValue:
< Combobox
items = {items}
itemToStringValue = {( item ) => item.customId}
paramName = "selection"
>
{ /* ... */ }
</ Combobox >
Display selected items as removable chips within the input.
Preview Code
import {
Combobox,
ComboboxChip,
ComboboxChipRemove,
ComboboxChips,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxItemIndicator,
ComboboxList,
ComboboxValue,
} from "@tilt-legal/cubitt-components/primitives" ;
const langs = [
{ id: "js" , value: "JavaScript" },
{ id: "ts" , value: "TypeScript" },
{ id: "py" , value: "Python" },
];
< Combobox items = {langs} multiple >
< ComboboxChips >
< ComboboxValue >
{( value ) => (
<>
{value. map (( language ) => (
< ComboboxChip key = {language.id} aria-label = {language.value}>
{language.value}
< ComboboxChipRemove />
</ ComboboxChip >
))}
< ComboboxInput placeholder = "e.g. TypeScript" />
</>
)}
</ ComboboxValue >
</ ComboboxChips >
< ComboboxContent >
< ComboboxEmpty >No languages found.</ ComboboxEmpty >
< ComboboxList >
{( language ) => (
< ComboboxItem key = {language.id} value = {language}>
< ComboboxItemIndicator />
{language.value}
</ ComboboxItem >
)}
</ ComboboxList >
</ ComboboxContent >
</ Combobox >;
Need users to create new tags on the fly? Use the TagInput component which provides a simpler API with built-in support for creating tags via Enter/comma, CSV paste parsing, and backspace removal.
Use a separate input field with chips displayed below.
Preview Code
< Combobox items = {langs} multiple >
< ComboboxWrapper >
< ComboboxInput placeholder = "e.g. TypeScript" />
< ComboboxClear />
< ComboboxIcon />
</ ComboboxWrapper >
< ComboboxChips className = "p-0! border-0 shadow-none" >
< ComboboxValue >
{( value ) => (
<>
{value. map (( language ) => (
< ComboboxChip key = {language.id} aria-label = {language.value}>
{language.value}
< ComboboxChipRemove />
</ ComboboxChip >
))}
</>
)}
</ ComboboxValue >
</ ComboboxChips >
< ComboboxContent >
< ComboboxEmpty >No languages found.</ ComboboxEmpty >
< ComboboxList >
{( language ) => (
< ComboboxItem key = {language.id} value = {language}>
< ComboboxItemIndicator />
{language.value}
</ ComboboxItem >
)}
</ ComboboxList >
</ ComboboxContent >
</ Combobox >
Use a button trigger with the input inside the popup.
Preview Code
import { Button } from "@tilt-legal/cubitt-components/primitives" ;
< Combobox items = {countries}>
< div className = "relative max-w-xs w-full" >
< ComboboxTrigger
className = "w-full"
render = {< Button variant = "outline" mode = "input" />}
>
< ComboboxValue >
{( value ) => <>{value ? value.label : "Select country" }</>}
</ ComboboxValue >
< ComboboxIcon />
</ ComboboxTrigger >
< ComboboxClear />
</ div >
< ComboboxContent >
< ComboboxInput placeholder = "e.g. United Kingdom" />
< ComboboxSeparator />
< ComboboxEmpty >No countries found.</ ComboboxEmpty >
< ComboboxList className = "overflow-y-auto max-h-[300px]" >
{( country ) => (
< ComboboxItem key = {country.code} value = {country}>
< ComboboxItemIndicator />
{country.label}
</ ComboboxItem >
)}
</ ComboboxList >
</ ComboboxContent >
</ Combobox >;
Use variant="light" when the popup sits on a lighter surface and should use bg-bg-2 with softer highlighted states.
Preview Code
< Combobox items = {matterTypes}>
< ComboboxWrapper >
< ComboboxInput placeholder = "e.g. Employment" />
< ComboboxClear />
< ComboboxIcon />
</ ComboboxWrapper >
< ComboboxContent variant = "light" >
< ComboboxEmpty >No matter types found.</ ComboboxEmpty >
< ComboboxList >
{( item ) => (
< ComboboxItem key = {item.value} value = {item}>
< ComboboxItemIndicator />
{item.label}
</ ComboboxItem >
)}
</ ComboboxList >
</ ComboboxContent >
</ Combobox >
Organize options into labeled groups.
Preview Code
import { ComboboxCollection, ComboboxGroup, ComboboxGroupLabel } from "@tilt-legal/cubitt-components/primitives" ;
const foodOptions = [
{
value: "Fruits" ,
items: [
{ value: "apple" , label: "Apple" },
{ value: "banana" , label: "Banana" },
],
},
{
value: "Vegetables" ,
items: [
{ value: "carrot" , label: "Carrot" },
{ value: "broccoli" , label: "Broccoli" },
],
},
];
< Combobox items = {foodOptions}>
< ComboboxWrapper >
< ComboboxInput placeholder = "e.g. Apple" />
< ComboboxClear />
< ComboboxIcon />
</ ComboboxWrapper >
< ComboboxContent className = "pt-0" >
< ComboboxEmpty >No results found.</ ComboboxEmpty >
< ComboboxList >
{( group ) => (
< ComboboxGroup key = {group.value} items = {group.items}>
< ComboboxGroupLabel >{group.value}</ ComboboxGroupLabel >
< ComboboxCollection >
{( item ) => (
< ComboboxItem key = {item.value} value = {item}>
< ComboboxItemIndicator />
{item.label}
</ ComboboxItem >
)}
</ ComboboxCollection >
</ ComboboxGroup >
)}
</ ComboboxList >
</ ComboboxContent >
</ Combobox >;
The combobox input supports three size variants: sm, md (default), and lg.
Preview Code
// Small
< ComboboxWrapper size = "sm" >
< ComboboxInput size = "sm" placeholder = "Small size" />
</ ComboboxWrapper >
// Medium (default)
< ComboboxWrapper >
< ComboboxInput placeholder = "Medium size" />
</ ComboboxWrapper >
// Large
< ComboboxWrapper size = "lg" >
< ComboboxInput size = "lg" placeholder = "Large size" />
</ ComboboxWrapper >
The indicator position can be changed to left or right.
Preview Code
import {
Combobox,
ComboboxContent,
ComboboxInput,
ComboboxItem,
ComboboxItemIndicator,
ComboboxList,
ComboboxWrapper,
} from "@tilt-legal/cubitt-components/primitives" ;
const fruits = [ "Apple" , "Banana" , "Orange" , "Mango" , "Strawberry" ];
< Combobox items = {fruits}>
< ComboboxWrapper >
< ComboboxInput placeholder = "Right indicator (default)..." />
</ ComboboxWrapper >
< ComboboxContent >
< ComboboxList >
{( item ) => (
< ComboboxItem key = {item} value = {item}>
< ComboboxItemIndicator />
{item}
</ ComboboxItem >
)}
</ ComboboxList >
</ ComboboxContent >
</ Combobox >
< Combobox items = {fruits} indicatorPosition = "left" >
< ComboboxWrapper >
< ComboboxInput placeholder = "Left indicator..." />
</ ComboboxWrapper >
< ComboboxContent >
< ComboboxList >
{( item ) => (
< ComboboxItem key = {item} value = {item}>
{item}
< ComboboxItemIndicator />
</ ComboboxItem >
)}
</ ComboboxList >
</ ComboboxContent >
</ Combobox >
Sync a single selection with the URL. Try selecting a category and refreshing the page.
Preview Code
import {
Combobox,
ComboboxClear,
ComboboxContent,
ComboboxEmpty,
ComboboxIcon,
ComboboxInput,
ComboboxItem,
ComboboxItemIndicator,
ComboboxList,
ComboboxWrapper,
Label,
} from "@tilt-legal/cubitt-components/primitives" ;
const categories = [
{ id: "electronics" , value: "Electronics" },
{ id: "books" , value: "Books" },
{ id: "clothing" , value: "Clothing" },
{ id: "home" , value: "Home & Garden" },
{ id: "sports" , value: "Sports" },
];
export default function Component () {
const id = React. useId ();
return (
< div className = "w-full max-w-xs flex flex-col gap-3" >
< Label htmlFor = {id}>Category</ Label >
< Combobox
items = {categories}
paramName = "demo-category" // Automatically uses category.id for URL
defaultValue = "electronics"
>
< ComboboxWrapper >
< ComboboxInput placeholder = "Select category..." id = {id} />
< ComboboxClear />
< ComboboxIcon />
</ ComboboxWrapper >
< ComboboxContent >
< ComboboxEmpty >No results found.</ ComboboxEmpty >
< ComboboxList >
{( item ) => (
< ComboboxItem key = {item.id} value = {item}>
< ComboboxItemIndicator />
{item.value}
</ ComboboxItem >
)}
</ ComboboxList >
</ ComboboxContent >
</ Combobox >
</ div >
);
}
Sync multiple selections with the URL using comma-separated values.
Preview Code
import {
Combobox,
ComboboxChip,
ComboboxChipRemove,
ComboboxChips,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxItemIndicator,
ComboboxList,
ComboboxValue,
Label,
} from "@tilt-legal/cubitt-components/primitives" ;
const tags = [
{ id: "react" , value: "React" },
{ id: "typescript" , value: "TypeScript" },
{ id: "tailwind" , value: "Tailwind CSS" },
{ id: "nextjs" , value: "Next.js" },
{ id: "nodejs" , value: "Node.js" },
{ id: "ui" , value: "UI/UX" },
];
export default function Component () {
const containerRef = React. useRef < HTMLDivElement | null >( null );
const id = React. useId ();
return (
< Combobox items = {tags} multiple paramName = "demo-tags" defaultValue = {[]}>
< div className = "w-full max-w-xs flex flex-col gap-3" >
< Label htmlFor = {id}>Tags</ Label >
< ComboboxChips ref = {containerRef}>
< ComboboxValue >
{( value ) => (
< React.Fragment >
{value. map (( tag ) => (
< ComboboxChip key = {tag.id} aria-label = {tag.value}>
{tag.value}
< ComboboxChipRemove />
</ ComboboxChip >
))}
< ComboboxInput
placeholder = {value. length > 0 ? "" : "Select tags..." }
id = {id}
/>
</ React.Fragment >
)}
</ ComboboxValue >
</ ComboboxChips >
</ div >
< ComboboxContent anchor = {containerRef}>
< ComboboxEmpty >No results found.</ ComboboxEmpty >
< ComboboxList >
{( tag ) => (
< ComboboxItem key = {tag.id} value = {tag}>
< ComboboxItemIndicator />
{tag.value}
</ ComboboxItem >
)}
</ ComboboxList >
</ ComboboxContent >
</ Combobox >
);
}
Prop Type Default Description itemsT[]— The list of items to display in the combobox. multiplebooleanfalseEnable multi-select mode. valueT | T[]— The controlled value(s). defaultValueT | T[]— The default value(s) when uncontrolled. onValueChange(value: T | T[]) => void— Callback when the selection changes. inputValuestring— The controlled input text value. onInputValueChange(value: string) => void— Callback when the input text changes. onOpenChange(open: boolean, details) => void— Callback when the popup open state changes. disabledbooleanfalseDisable the combobox. indicatorPosition"left" | "right""right"Position of the selection indicator in items. classNamestring— Additional CSS classes for the combobox.
When provided with a paramName, the combobox will sync its selection with URL parameters via TanStack Router search params.
Prop Type Default Description paramNamestring— URL parameter name for syncing state with URL. Enables URL state management. onUrlValueChange(value: T | T[] | null) => void— Callback when URL parameter value changes. paramClearOnDefaultbooleantrueRemove URL param when value equals default. paramThrottlenumber— Throttle URL updates in milliseconds. paramDebouncenumber— Debounce URL updates in milliseconds. itemToStringValue(item: T) => string— Function to extract string value from item for URL serialization. Auto-detects common ID fields if not provided.
Prop Type Description size"sm" | "md" | "lg"The size variant of the input. placeholderstringPlaceholder text for the input. classNamestringAdditional CSS classes.
Prop Type Description valueTThe value this item represents. classNamestringAdditional CSS classes.
Prop Type Description size"sm" | "md" | "lg"The size variant matching the input. classNamestringAdditional CSS classes.
A wrapper component that provides input styling and serves as the positioning anchor for the popup. Uses the same styles as InputWrapper for consistency.
Prop Type Description size"sm" | "md" | "lg"The size variant matching the input. classNamestringAdditional CSS classes.
Prop Type Default Description variant"default" | "light""default"Controls popup and highlighted styling. classNamestring— Additional CSS classes.
Prop Type Description classNamestringAdditional CSS classes.
Prop Type Description classNamestringAdditional CSS classes.
Prop Type Description classNamestringAdditional CSS classes.
Prop Type Description classNamestringAdditional CSS classes.
Prop Type Description classNamestringAdditional CSS classes.
Prop Type Description classNamestringAdditional CSS classes.
Prop Type Description classNamestringAdditional CSS classes.
Prop Type Description childrenReactNode | ((value: T | T[]) => ReactNode)Content or render function for displaying value. classNamestringAdditional CSS classes.
Prop Type Description classNamestringAdditional CSS classes.
Prop Type Description classNamestringAdditional CSS classes.
Prop Type Description classNamestringAdditional CSS classes.