Combobox
An input combined with a list of predefined items to select.
import { createUniqueId } from 'solid-js';
import type { JSX } from '@solidjs/web';
import { Combobox } from 'base-ui-solid/combobox';
import styles from './index.module.css';
export default function ExampleCombobox() {
const id = createUniqueId();
return (
<Combobox.Root items={fruits}>
<div class={styles.Label}>
<label for={id}>Choose a fruit</label>
<Combobox.InputGroup class={styles.InputGroup}>
<Combobox.Input placeholder="e.g. Apple" id={id} class={styles.Input} />
<div class={styles.ActionButtons}>
<Combobox.Clear class={styles.Clear} aria-label="Clear selection">
<XIcon />
</Combobox.Clear>
<Combobox.Trigger class={styles.Trigger} aria-label="Open popup">
<CaretDownIcon />
</Combobox.Trigger>
</div>
</Combobox.InputGroup>
</div>
<Combobox.Portal>
<Combobox.Positioner class={styles.Positioner} sideOffset={4}>
<Combobox.Popup class={styles.Popup}>
<Combobox.Empty>
<div class={styles.Empty}>No fruits found.</div>
</Combobox.Empty>
<Combobox.List class={styles.List}>
{(item: Fruit) => (
<Combobox.Item value={item} class={styles.Item}>
<Combobox.ItemIndicator class={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
<span class={styles.ItemText}>{item.label}</span>
</Combobox.Item>
)}
</Combobox.List>
</Combobox.Popup>
</Combobox.Positioner>
</Combobox.Portal>
</Combobox.Root>
);
}
function CheckIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="m2.5 8.5 4 4 7-9" />
</svg>
);
}
function XIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-linecap="square"
stroke-linejoin="round"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="m4.5 4.5 7 7m-7 0 7-7" />
</svg>
);
}
function CaretDownIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="currentColor"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="M12 6H4l4 4.5z" />
</svg>
);
}
interface Fruit {
label: string;
value: string;
}
const fruits: Fruit[] = [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Orange', value: 'orange' },
{ label: 'Pineapple', value: 'pineapple' },
{ label: 'Grape', value: 'grape' },
{ label: 'Mango', value: 'mango' },
{ label: 'Strawberry', value: 'strawberry' },
{ label: 'Blueberry', value: 'blueberry' },
{ label: 'Raspberry', value: 'raspberry' },
{ label: 'Blackberry', value: 'blackberry' },
{ label: 'Cherry', value: 'cherry' },
{ label: 'Peach', value: 'peach' },
{ label: 'Pear', value: 'pear' },
{ label: 'Plum', value: 'plum' },
{ label: 'Kiwi', value: 'kiwi' },
{ label: 'Watermelon', value: 'watermelon' },
{ label: 'Cantaloupe', value: 'cantaloupe' },
{ label: 'Honeydew', value: 'honeydew' },
{ label: 'Papaya', value: 'papaya' },
{ label: 'Guava', value: 'guava' },
{ label: 'Lychee', value: 'lychee' },
{ label: 'Pomegranate', value: 'pomegranate' },
{ label: 'Apricot', value: 'apricot' },
{ label: 'Grapefruit', value: 'grapefruit' },
{ label: 'Passionfruit', value: 'passionfruit' },
];
Usage guidelines
- Combobox is a filterable Select: Use Combobox when the input is restricted to a set of predefined selectable items, similar to Select but whose items are filterable using an input. Prefer using Combobox over Select when the number of items is sufficiently large to warrant filtering.
- Avoid for simple search widgets: Combobox does not allow free-form text input. For search widgets, consider using Autocomplete instead.
- Avoid when not rendering an input: Use Select instead of Combobox if no input is being rendered, which includes accessibility features specific to a listbox without an input.
- Form controls must have an accessible name: If
<Combobox.Input>is the form control, label it with a native<label>or<Field.Label>, or provide anaria-labelwhen no visible label is rendered.<Combobox.Label>labels<Combobox.Trigger>and is intended for the input-inside-popup pattern, where the trigger is the form control. See the forms guide. - Closing animations: The popup stays rendered until its closing animation finishes. See JavaScript animations for animating it with Motion and for manual control.
Anatomy
Import the components and place them together:
import { Combobox } from 'base-ui-solid/combobox';
<Combobox.Root>
<Combobox.Label />
<Combobox.InputGroup>
<Combobox.Input />
<Combobox.Trigger />
<Combobox.Icon />
<Combobox.Clear />
<Combobox.Value />
<Combobox.Chips>
<Combobox.Chip>
<Combobox.ChipRemove />
</Combobox.Chip>
</Combobox.Chips>
</Combobox.InputGroup>
<Combobox.Portal>
<Combobox.Backdrop />
<Combobox.Positioner>
<Combobox.Popup>
<Combobox.Arrow />
<Combobox.Status />
<Combobox.Empty />
<Combobox.List>
<Combobox.Row>
<Combobox.Item>
<Combobox.ItemIndicator />
</Combobox.Item>
</Combobox.Row>
<Combobox.Separator />
<Combobox.Group>
<Combobox.GroupLabel />
</Combobox.Group>
<Combobox.Collection />
</Combobox.List>
</Combobox.Popup>
</Combobox.Positioner>
</Combobox.Portal>
</Combobox.Root>;
Item values
Each <Combobox.Item> takes a value prop identifying it. Pass the item being rendered:
<Combobox.List>{(item) => <Combobox.Item value={item}>{item.label}</Combobox.Item>}</Combobox.List>
That item is what value, defaultValue, and onValueChange receive. To store IDs instead, see Value selection with IDs.
Examples
Typed wrapper component
The following example shows a typed wrapper around the Combobox component with correct type inference and type safety:
import * as Solid from 'react';
import { Combobox } from 'base-ui-solid/combobox';
export function MyCombobox<Value, Multiple extends boolean | undefined = false, Item = Value>(
props: Combobox.Root.Props<Value, Multiple, Item>,
): JSX.Element {
return <Combobox.Root {...props}>{/* ... */}</Combobox.Root>;
}
The third Item type parameter is what lets the wrapper accept a Combobox.createItems() collection, whose rendered item type differs from its selection value. Omit it and a collection infers Value as the source item type, surfacing an error on defaultValue rather than on items.
Value selection with IDs
When your app stores IDs rather than item objects, use Combobox.createItems with getValue and getLabel to derive each item’s selection value and display label.
With static data, create the collection at module scope. The accessor parameters are inferred from users. Pass the derived ID to the value prop of <Combobox.Item>, not the item being rendered:
const items = Combobox.createItems(users, {
getValue: (user) => user.id,
getLabel: (user) => user.name,
});
<Combobox.Root items={items}>
<Combobox.List>
{(user) => <Combobox.Item value={user.id}>{user.name}</Combobox.Item>}
</Combobox.List>
</Combobox.Root>;
Selection props and events use the derived IDs, while list rendering continues to receive the original items. The derived label is also used for filtering and typeahead.
When the data is loaded or replaced at runtime, create the collection in a memo so that it is rebuilt only when the data changes:
const items = createMemo(() =>
Combobox.createItems(users(), {
getValue: (user) => user.id,
getLabel: (user) => user.name,
}),
);
<Combobox.Root items={items()} />;
import { createUniqueId } from 'solid-js';
import type { JSX } from '@solidjs/web';
import { Combobox } from 'base-ui-solid/combobox';
import styles from './index.module.css';
export default function ExampleCreateItemsCombobox() {
const id = createUniqueId();
return (
<Combobox.Root items={items} defaultValue="banana">
<div class={styles.Label}>
<label for={id}>Choose a fruit</label>
<Combobox.InputGroup class={styles.InputGroup}>
<Combobox.Input placeholder="e.g. Apple" id={id} class={styles.Input} />
<div class={styles.ActionButtons}>
<Combobox.Clear class={styles.Clear} aria-label="Clear selection">
<XIcon />
</Combobox.Clear>
<Combobox.Trigger class={styles.Trigger} aria-label="Open popup">
<CaretDownIcon />
</Combobox.Trigger>
</div>
</Combobox.InputGroup>
</div>
<Combobox.Portal>
<Combobox.Positioner class={styles.Positioner} sideOffset={4}>
<Combobox.Popup class={styles.Popup}>
<Combobox.Empty>
<div class={styles.Empty}>No fruits found.</div>
</Combobox.Empty>
<Combobox.List class={styles.List}>
{(item) => (
<Combobox.Item value={item.id} class={styles.Item}>
<Combobox.ItemIndicator class={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
<span class={styles.ItemText}>{item.name}</span>
</Combobox.Item>
)}
</Combobox.List>
</Combobox.Popup>
</Combobox.Positioner>
</Combobox.Portal>
</Combobox.Root>
);
}
function CheckIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="m2.5 8.5 4 4 7-9" />
</svg>
);
}
function XIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-linecap="square"
stroke-linejoin="round"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="m4.5 4.5 7 7m-7 0 7-7" />
</svg>
);
}
function CaretDownIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="currentColor"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="M12 6H4l4 4.5z" />
</svg>
);
}
interface Fruit {
id: string;
name: string;
}
const fruits: Fruit[] = [
{ id: 'apple', name: 'Apple' },
{ id: 'banana', name: 'Banana' },
{ id: 'orange', name: 'Orange' },
{ id: 'pineapple', name: 'Pineapple' },
{ id: 'grape', name: 'Grape' },
{ id: 'mango', name: 'Mango' },
{ id: 'strawberry', name: 'Strawberry' },
{ id: 'blueberry', name: 'Blueberry' },
{ id: 'raspberry', name: 'Raspberry' },
{ id: 'blackberry', name: 'Blackberry' },
{ id: 'cherry', name: 'Cherry' },
{ id: 'peach', name: 'Peach' },
{ id: 'pear', name: 'Pear' },
{ id: 'plum', name: 'Plum' },
{ id: 'kiwi', name: 'Kiwi' },
{ id: 'watermelon', name: 'Watermelon' },
];
const items = Combobox.createItems(fruits, {
getValue: (fruit) => fruit.id,
getLabel: (fruit) => fruit.name,
});
As a rule of thumb, use primitive items for simple lists, object values when the selected record is useful application state, and createItems() when selection should use a stable primitive ID while rendering and filtering still use object records.
Stable IDs preserve item matching when an async data library replaces row objects during a refetch. With object values, use isItemEqualToValue to compare their IDs instead.
When results are filtered externally (for example, by a server-side search), items represents records known to the app, while filteredItems represents the current result window displayed in the popup. Pass source items rather than derived values to filteredItems, preserving the flat or grouped structure of items:
const items = createMemo(() =>
Combobox.createItems(knownUsers(), {
getValue: (user) => user.id,
getLabel: (user) => user.name,
}),
);
<Combobox.Root
items={items()}
filteredItems={searchResults()}
value={selectedUserId()}
onValueChange={setSelectedUserId}
/>;
Include the selected user’s record in knownUsers so its label remains available when it is outside searchResults. Alternatively, provide itemToStringLabel when the label can be derived from the selected value alone.
Multiple select
The combobox can allow multiple selections by adding the multiple prop to <Combobox.Root>.
Selection chips are rendered with <Combobox.Chip> inside the input that can be removed.
import { createUniqueId, For } from 'solid-js';
import type { JSX } from '@solidjs/web';
import { Combobox } from 'base-ui-solid/combobox';
import styles from './index.module.css';
export default function ExampleMultipleCombobox() {
const id = createUniqueId();
return (
<Combobox.Root items={langs} multiple>
<div class={styles.Container}>
<label class={styles.Label} for={id}>
Programming languages
</label>
<Combobox.InputGroup class={styles.InputGroup}>
<Combobox.Value>
{(value: ProgrammingLanguage[]) => (
<Combobox.Chips
class={styles.Chips}
aria-label={value.length > 0 ? 'Selected languages' : undefined}
>
<For each={value}>
{(language) => (
<Combobox.Chip
class={styles.Chip}
aria-label={language.value}
aria-description="Press Backspace or Delete to remove"
>
{language.value}
<Combobox.ChipRemove
class={styles.ChipRemove}
aria-label={`Remove ${language.value}`}
>
<XIcon />
</Combobox.ChipRemove>
</Combobox.Chip>
)}
</For>
<Combobox.Input
id={id}
placeholder={value.length > 0 ? '' : 'e.g. TypeScript'}
aria-description={
value.length > 0
? `${value.length} selected. From the start of the input, press Left Arrow to focus the selected items`
: undefined
}
class={styles.Input}
/>
</Combobox.Chips>
)}
</Combobox.Value>
</Combobox.InputGroup>
</div>
<Combobox.Portal>
<Combobox.Positioner class={styles.Positioner} sideOffset={4}>
<Combobox.Popup class={styles.Popup}>
<Combobox.Empty>
<div class={styles.Empty}>No languages found.</div>
</Combobox.Empty>
<Combobox.List>
{(language: ProgrammingLanguage) => (
<Combobox.Item class={styles.Item} value={language}>
<Combobox.ItemIndicator class={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
<span class={styles.ItemText}>{language.value}</span>
</Combobox.Item>
)}
</Combobox.List>
</Combobox.Popup>
</Combobox.Positioner>
</Combobox.Portal>
</Combobox.Root>
);
}
function CheckIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="m2.5 8.5 4 4 7-9" />
</svg>
);
}
function XIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-linecap="square"
stroke-linejoin="round"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="m4.5 4.5 7 7m-7 0 7-7" />
</svg>
);
}
interface ProgrammingLanguage {
id: string;
value: string;
}
const langs: ProgrammingLanguage[] = [
{ id: 'js', value: 'JavaScript' },
{ id: 'ts', value: 'TypeScript' },
{ id: 'py', value: 'Python' },
{ id: 'java', value: 'Java' },
{ id: 'cpp', value: 'C++' },
{ id: 'cs', value: 'C#' },
{ id: 'php', value: 'PHP' },
{ id: 'ruby', value: 'Ruby' },
{ id: 'go', value: 'Go' },
{ id: 'rust', value: 'Rust' },
{ id: 'swift', value: 'Swift' },
];
In order for screen readers to announce how to reach and remove the chips, add aria-description to <Combobox.Chip> and <Combobox.Input>, alongside the aria-label on <Combobox.Chips> and <Combobox.ChipRemove>.
Base UI does not ship these strings. Translate them together with the rest of your interface.
Visible chips can be limited by slicing the selected values rendered in <Combobox.Value>:
const CHIP_LIMIT = 3;
<Combobox.Value>
{(selectedValue: string[]) => {
const visibleValue = selectedValue.slice(0, CHIP_LIMIT);
const hiddenCount = selectedValue.length - visibleValue.length;
return (
<>
<For each={visibleValue}>
{(item) => (
<Combobox.Chip aria-description="Press Backspace or Delete to remove">
{item}
<Combobox.ChipRemove aria-label={`Remove ${item}`} />
</Combobox.Chip>
)}
</For>
{hiddenCount > 0 && <span>{`+${hiddenCount} more`}</span>}
<Combobox.Input
aria-description={
selectedValue.length > 0
? `${selectedValue.length} selected. From the start of the input, press Left Arrow to focus the selected items`
: undefined
}
/>
</>
);
}}
</Combobox.Value>;
Keeping the filter after selection
In multiple mode, selecting an item clears the typed filter and closes the popup when the input is rendered outside it. To let users pick several results from one query, cancel the relevant change request.
Which request to cancel depends on the input’s placement. When the input is outside the popup, cancel the item-press close request in onOpenChange. When it is inside, cancel the clear request in onInputValueChange, marked with eventDetails.isItemPress.
<Combobox.Root
multiple
onOpenChange={(open, eventDetails) => {
if (!open && eventDetails.reason === 'item-press') {
eventDetails.cancel();
}
}}
>
{/* ... */}
</Combobox.Root>
<Combobox.Root
multiple
onInputValueChange={(value, eventDetails) => {
if (eventDetails.isItemPress) {
eventDetails.cancel();
}
}}
>
{/* ... */}
</Combobox.Root>
The typed filter still resets once the popup closes.
Input inside popup
<Combobox.Input> can be rendered inside <Combobox.Popup> to create a searchable select popup.
import type { JSX } from '@solidjs/web';
import { Combobox } from 'base-ui-solid/combobox';
import styles from './index.module.css';
export default function ExamplePopoverCombobox() {
return (
<div class={styles.Field}>
<Combobox.Root items={countries}>
<Combobox.Label class={styles.Label}>Country</Combobox.Label>
<Combobox.Trigger class={styles.Trigger}>
<Combobox.Value placeholder="Select country" />
<Combobox.Icon class={styles.TriggerIcon}>
<CaretUpDownIcon />
</Combobox.Icon>
</Combobox.Trigger>
<Combobox.Portal>
<Combobox.Positioner align="start" sideOffset={4}>
<Combobox.Popup class={styles.Popup} aria-label="Select country">
<Combobox.Input placeholder="e.g. United Kingdom" class={styles.Input} />
<div class={styles.Viewport}>
<Combobox.Empty>
<div class={styles.Empty}>No countries found.</div>
</Combobox.Empty>
<Combobox.List class={styles.List}>
{(country: Country) => (
<Combobox.Item value={country} class={styles.Item}>
<Combobox.ItemIndicator class={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
<span class={styles.ItemText}>{country.label}</span>
</Combobox.Item>
)}
</Combobox.List>
</div>
</Combobox.Popup>
</Combobox.Positioner>
</Combobox.Portal>
</Combobox.Root>
</div>
);
}
function CaretUpDownIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="currentColor"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="M11 10H5l3 3.5zm0-4H5l3-3.5z" />
</svg>
);
}
function CheckIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="m2.5 8.5 4 4 7-9" />
</svg>
);
}
interface Country {
code: string;
value: string;
continent: string;
label: string;
}
const countries: Country[] = [
{ code: 'af', value: 'afghanistan', label: 'Afghanistan', continent: 'Asia' },
{ code: 'al', value: 'albania', label: 'Albania', continent: 'Europe' },
{ code: 'dz', value: 'algeria', label: 'Algeria', continent: 'Africa' },
{ code: 'ad', value: 'andorra', label: 'Andorra', continent: 'Europe' },
{ code: 'ao', value: 'angola', label: 'Angola', continent: 'Africa' },
{ code: 'ar', value: 'argentina', label: 'Argentina', continent: 'South America' },
{ code: 'am', value: 'armenia', label: 'Armenia', continent: 'Asia' },
{ code: 'au', value: 'australia', label: 'Australia', continent: 'Oceania' },
{ code: 'at', value: 'austria', label: 'Austria', continent: 'Europe' },
{ code: 'az', value: 'azerbaijan', label: 'Azerbaijan', continent: 'Asia' },
{ code: 'bs', value: 'bahamas', label: 'Bahamas', continent: 'North America' },
{ code: 'bh', value: 'bahrain', label: 'Bahrain', continent: 'Asia' },
{ code: 'bd', value: 'bangladesh', label: 'Bangladesh', continent: 'Asia' },
{ code: 'bb', value: 'barbados', label: 'Barbados', continent: 'North America' },
{ code: 'by', value: 'belarus', label: 'Belarus', continent: 'Europe' },
{ code: 'be', value: 'belgium', label: 'Belgium', continent: 'Europe' },
{ code: 'bz', value: 'belize', label: 'Belize', continent: 'North America' },
{ code: 'bj', value: 'benin', label: 'Benin', continent: 'Africa' },
{ code: 'bt', value: 'bhutan', label: 'Bhutan', continent: 'Asia' },
{ code: 'bo', value: 'bolivia', label: 'Bolivia', continent: 'South America' },
{
code: 'ba',
value: 'bosnia-and-herzegovina',
label: 'Bosnia and Herzegovina',
continent: 'Europe',
},
{ code: 'bw', value: 'botswana', label: 'Botswana', continent: 'Africa' },
{ code: 'br', value: 'brazil', label: 'Brazil', continent: 'South America' },
{ code: 'bn', value: 'brunei', label: 'Brunei', continent: 'Asia' },
{ code: 'bg', value: 'bulgaria', label: 'Bulgaria', continent: 'Europe' },
{ code: 'bf', value: 'burkina-faso', label: 'Burkina Faso', continent: 'Africa' },
{ code: 'bi', value: 'burundi', label: 'Burundi', continent: 'Africa' },
{ code: 'kh', value: 'cambodia', label: 'Cambodia', continent: 'Asia' },
{ code: 'cm', value: 'cameroon', label: 'Cameroon', continent: 'Africa' },
{ code: 'ca', value: 'canada', label: 'Canada', continent: 'North America' },
{ code: 'cv', value: 'cape-verde', label: 'Cape Verde', continent: 'Africa' },
{
code: 'cf',
value: 'central-african-republic',
label: 'Central African Republic',
continent: 'Africa',
},
{ code: 'td', value: 'chad', label: 'Chad', continent: 'Africa' },
{ code: 'cl', value: 'chile', label: 'Chile', continent: 'South America' },
{ code: 'cn', value: 'china', label: 'China', continent: 'Asia' },
{ code: 'co', value: 'colombia', label: 'Colombia', continent: 'South America' },
{ code: 'km', value: 'comoros', label: 'Comoros', continent: 'Africa' },
{ code: 'cg', value: 'congo', label: 'Congo', continent: 'Africa' },
{ code: 'cr', value: 'costa-rica', label: 'Costa Rica', continent: 'North America' },
{ code: 'hr', value: 'croatia', label: 'Croatia', continent: 'Europe' },
{ code: 'cu', value: 'cuba', label: 'Cuba', continent: 'North America' },
{ code: 'cy', value: 'cyprus', label: 'Cyprus', continent: 'Asia' },
{ code: 'cz', value: 'czech-republic', label: 'Czech Republic', continent: 'Europe' },
{ code: 'dk', value: 'denmark', label: 'Denmark', continent: 'Europe' },
{ code: 'dj', value: 'djibouti', label: 'Djibouti', continent: 'Africa' },
{ code: 'dm', value: 'dominica', label: 'Dominica', continent: 'North America' },
{
code: 'do',
value: 'dominican-republic',
label: 'Dominican Republic',
continent: 'North America',
},
{ code: 'ec', value: 'ecuador', label: 'Ecuador', continent: 'South America' },
{ code: 'eg', value: 'egypt', label: 'Egypt', continent: 'Africa' },
{ code: 'sv', value: 'el-salvador', label: 'El Salvador', continent: 'North America' },
{ code: 'gq', value: 'equatorial-guinea', label: 'Equatorial Guinea', continent: 'Africa' },
{ code: 'er', value: 'eritrea', label: 'Eritrea', continent: 'Africa' },
{ code: 'ee', value: 'estonia', label: 'Estonia', continent: 'Europe' },
{ code: 'et', value: 'ethiopia', label: 'Ethiopia', continent: 'Africa' },
{ code: 'fj', value: 'fiji', label: 'Fiji', continent: 'Oceania' },
{ code: 'fi', value: 'finland', label: 'Finland', continent: 'Europe' },
{ code: 'fr', value: 'france', label: 'France', continent: 'Europe' },
{ code: 'ga', value: 'gabon', label: 'Gabon', continent: 'Africa' },
{ code: 'gm', value: 'gambia', label: 'Gambia', continent: 'Africa' },
{ code: 'ge', value: 'georgia', label: 'Georgia', continent: 'Asia' },
{ code: 'de', value: 'germany', label: 'Germany', continent: 'Europe' },
{ code: 'gh', value: 'ghana', label: 'Ghana', continent: 'Africa' },
{ code: 'gr', value: 'greece', label: 'Greece', continent: 'Europe' },
{ code: 'gd', value: 'grenada', label: 'Grenada', continent: 'North America' },
{ code: 'gt', value: 'guatemala', label: 'Guatemala', continent: 'North America' },
{ code: 'gn', value: 'guinea', label: 'Guinea', continent: 'Africa' },
{ code: 'gw', value: 'guinea-bissau', label: 'Guinea-Bissau', continent: 'Africa' },
{ code: 'gy', value: 'guyana', label: 'Guyana', continent: 'South America' },
{ code: 'ht', value: 'haiti', label: 'Haiti', continent: 'North America' },
{ code: 'hn', value: 'honduras', label: 'Honduras', continent: 'North America' },
{ code: 'hu', value: 'hungary', label: 'Hungary', continent: 'Europe' },
{ code: 'is', value: 'iceland', label: 'Iceland', continent: 'Europe' },
{ code: 'in', value: 'india', label: 'India', continent: 'Asia' },
{ code: 'id', value: 'indonesia', label: 'Indonesia', continent: 'Asia' },
{ code: 'ir', value: 'iran', label: 'Iran', continent: 'Asia' },
{ code: 'iq', value: 'iraq', label: 'Iraq', continent: 'Asia' },
{ code: 'ie', value: 'ireland', label: 'Ireland', continent: 'Europe' },
{ code: 'il', value: 'israel', label: 'Israel', continent: 'Asia' },
{ code: 'it', value: 'italy', label: 'Italy', continent: 'Europe' },
{ code: 'jm', value: 'jamaica', label: 'Jamaica', continent: 'North America' },
{ code: 'jp', value: 'japan', label: 'Japan', continent: 'Asia' },
{ code: 'jo', value: 'jordan', label: 'Jordan', continent: 'Asia' },
{ code: 'kz', value: 'kazakhstan', label: 'Kazakhstan', continent: 'Asia' },
{ code: 'ke', value: 'kenya', label: 'Kenya', continent: 'Africa' },
{ code: 'kw', value: 'kuwait', label: 'Kuwait', continent: 'Asia' },
{ code: 'kg', value: 'kyrgyzstan', label: 'Kyrgyzstan', continent: 'Asia' },
{ code: 'la', value: 'laos', label: 'Laos', continent: 'Asia' },
{ code: 'lv', value: 'latvia', label: 'Latvia', continent: 'Europe' },
{ code: 'lb', value: 'lebanon', label: 'Lebanon', continent: 'Asia' },
{ code: 'ls', value: 'lesotho', label: 'Lesotho', continent: 'Africa' },
{ code: 'lr', value: 'liberia', label: 'Liberia', continent: 'Africa' },
{ code: 'ly', value: 'libya', label: 'Libya', continent: 'Africa' },
{ code: 'li', value: 'liechtenstein', label: 'Liechtenstein', continent: 'Europe' },
{ code: 'lt', value: 'lithuania', label: 'Lithuania', continent: 'Europe' },
{ code: 'lu', value: 'luxembourg', label: 'Luxembourg', continent: 'Europe' },
{ code: 'mg', value: 'madagascar', label: 'Madagascar', continent: 'Africa' },
{ code: 'mw', value: 'malawi', label: 'Malawi', continent: 'Africa' },
{ code: 'my', value: 'malaysia', label: 'Malaysia', continent: 'Asia' },
{ code: 'mv', value: 'maldives', label: 'Maldives', continent: 'Asia' },
{ code: 'ml', value: 'mali', label: 'Mali', continent: 'Africa' },
{ code: 'mt', value: 'malta', label: 'Malta', continent: 'Europe' },
{ code: 'mh', value: 'marshall-islands', label: 'Marshall Islands', continent: 'Oceania' },
{ code: 'mr', value: 'mauritania', label: 'Mauritania', continent: 'Africa' },
{ code: 'mu', value: 'mauritius', label: 'Mauritius', continent: 'Africa' },
{ code: 'mx', value: 'mexico', label: 'Mexico', continent: 'North America' },
{ code: 'fm', value: 'micronesia', label: 'Micronesia', continent: 'Oceania' },
{ code: 'md', value: 'moldova', label: 'Moldova', continent: 'Europe' },
{ code: 'mc', value: 'monaco', label: 'Monaco', continent: 'Europe' },
{ code: 'mn', value: 'mongolia', label: 'Mongolia', continent: 'Asia' },
{ code: 'me', value: 'montenegro', label: 'Montenegro', continent: 'Europe' },
{ code: 'ma', value: 'morocco', label: 'Morocco', continent: 'Africa' },
{ code: 'mz', value: 'mozambique', label: 'Mozambique', continent: 'Africa' },
{ code: 'mm', value: 'myanmar', label: 'Myanmar', continent: 'Asia' },
{ code: 'na', value: 'namibia', label: 'Namibia', continent: 'Africa' },
{ code: 'nr', value: 'nauru', label: 'Nauru', continent: 'Oceania' },
{ code: 'np', value: 'nepal', label: 'Nepal', continent: 'Asia' },
{ code: 'nl', value: 'netherlands', label: 'Netherlands', continent: 'Europe' },
{ code: 'nz', value: 'new-zealand', label: 'New Zealand', continent: 'Oceania' },
{ code: 'ni', value: 'nicaragua', label: 'Nicaragua', continent: 'North America' },
{ code: 'ne', value: 'niger', label: 'Niger', continent: 'Africa' },
{ code: 'ng', value: 'nigeria', label: 'Nigeria', continent: 'Africa' },
{ code: 'kp', value: 'north-korea', label: 'North Korea', continent: 'Asia' },
{ code: 'mk', value: 'north-macedonia', label: 'North Macedonia', continent: 'Europe' },
{ code: 'no', value: 'norway', label: 'Norway', continent: 'Europe' },
{ code: 'om', value: 'oman', label: 'Oman', continent: 'Asia' },
{ code: 'pk', value: 'pakistan', label: 'Pakistan', continent: 'Asia' },
{ code: 'pw', value: 'palau', label: 'Palau', continent: 'Oceania' },
{ code: 'ps', value: 'palestine', label: 'Palestine', continent: 'Asia' },
{ code: 'pa', value: 'panama', label: 'Panama', continent: 'North America' },
{ code: 'pg', value: 'papua-new-guinea', label: 'Papua New Guinea', continent: 'Oceania' },
{ code: 'py', value: 'paraguay', label: 'Paraguay', continent: 'South America' },
{ code: 'pe', value: 'peru', label: 'Peru', continent: 'South America' },
{ code: 'ph', value: 'philippines', label: 'Philippines', continent: 'Asia' },
{ code: 'pl', value: 'poland', label: 'Poland', continent: 'Europe' },
{ code: 'pt', value: 'portugal', label: 'Portugal', continent: 'Europe' },
{ code: 'qa', value: 'qatar', label: 'Qatar', continent: 'Asia' },
{ code: 'ro', value: 'romania', label: 'Romania', continent: 'Europe' },
{ code: 'ru', value: 'russia', label: 'Russia', continent: 'Europe' },
{ code: 'rw', value: 'rwanda', label: 'Rwanda', continent: 'Africa' },
{ code: 'ws', value: 'samoa', label: 'Samoa', continent: 'Oceania' },
{ code: 'sm', value: 'san-marino', label: 'San Marino', continent: 'Europe' },
{ code: 'sa', value: 'saudi-arabia', label: 'Saudi Arabia', continent: 'Asia' },
{ code: 'sn', value: 'senegal', label: 'Senegal', continent: 'Africa' },
{ code: 'rs', value: 'serbia', label: 'Serbia', continent: 'Europe' },
{ code: 'sc', value: 'seychelles', label: 'Seychelles', continent: 'Africa' },
{ code: 'sl', value: 'sierra-leone', label: 'Sierra Leone', continent: 'Africa' },
{ code: 'sg', value: 'singapore', label: 'Singapore', continent: 'Asia' },
{ code: 'sk', value: 'slovakia', label: 'Slovakia', continent: 'Europe' },
{ code: 'si', value: 'slovenia', label: 'Slovenia', continent: 'Europe' },
{ code: 'sb', value: 'solomon-islands', label: 'Solomon Islands', continent: 'Oceania' },
{ code: 'so', value: 'somalia', label: 'Somalia', continent: 'Africa' },
{ code: 'za', value: 'south-africa', label: 'South Africa', continent: 'Africa' },
{ code: 'kr', value: 'south-korea', label: 'South Korea', continent: 'Asia' },
{ code: 'ss', value: 'south-sudan', label: 'South Sudan', continent: 'Africa' },
{ code: 'es', value: 'spain', label: 'Spain', continent: 'Europe' },
{ code: 'lk', value: 'sri-lanka', label: 'Sri Lanka', continent: 'Asia' },
{ code: 'sd', value: 'sudan', label: 'Sudan', continent: 'Africa' },
{ code: 'sr', value: 'suriname', label: 'Suriname', continent: 'South America' },
{ code: 'se', value: 'sweden', label: 'Sweden', continent: 'Europe' },
{ code: 'ch', value: 'switzerland', label: 'Switzerland', continent: 'Europe' },
{ code: 'sy', value: 'syria', label: 'Syria', continent: 'Asia' },
{ code: 'tw', value: 'taiwan', label: 'Taiwan', continent: 'Asia' },
{ code: 'tj', value: 'tajikistan', label: 'Tajikistan', continent: 'Asia' },
{ code: 'tz', value: 'tanzania', label: 'Tanzania', continent: 'Africa' },
{ code: 'th', value: 'thailand', label: 'Thailand', continent: 'Asia' },
{ code: 'tl', value: 'timor-leste', label: 'Timor-Leste', continent: 'Asia' },
{ code: 'tg', value: 'togo', label: 'Togo', continent: 'Africa' },
{ code: 'to', value: 'tonga', label: 'Tonga', continent: 'Oceania' },
{
code: 'tt',
value: 'trinidad-and-tobago',
label: 'Trinidad and Tobago',
continent: 'North America',
},
{ code: 'tn', value: 'tunisia', label: 'Tunisia', continent: 'Africa' },
{ code: 'tr', value: 'turkey', label: 'Turkey', continent: 'Asia' },
{ code: 'tm', value: 'turkmenistan', label: 'Turkmenistan', continent: 'Asia' },
{ code: 'tv', value: 'tuvalu', label: 'Tuvalu', continent: 'Oceania' },
{ code: 'ug', value: 'uganda', label: 'Uganda', continent: 'Africa' },
{ code: 'ua', value: 'ukraine', label: 'Ukraine', continent: 'Europe' },
{ code: 'ae', value: 'united-arab-emirates', label: 'United Arab Emirates', continent: 'Asia' },
{ code: 'gb', value: 'united-kingdom', label: 'United Kingdom', continent: 'Europe' },
{ code: 'us', value: 'united-states', label: 'United States', continent: 'North America' },
{ code: 'uy', value: 'uruguay', label: 'Uruguay', continent: 'South America' },
{ code: 'uz', value: 'uzbekistan', label: 'Uzbekistan', continent: 'Asia' },
{ code: 'vu', value: 'vanuatu', label: 'Vanuatu', continent: 'Oceania' },
{ code: 'va', value: 'vatican-city', label: 'Vatican City', continent: 'Europe' },
{ code: 've', value: 'venezuela', label: 'Venezuela', continent: 'South America' },
{ code: 'vn', value: 'vietnam', label: 'Vietnam', continent: 'Asia' },
{ code: 'ye', value: 'yemen', label: 'Yemen', continent: 'Asia' },
{ code: 'zm', value: 'zambia', label: 'Zambia', continent: 'Africa' },
{ code: 'zw', value: 'zimbabwe', label: 'Zimbabwe', continent: 'Africa' },
];
Use <Combobox.Label> to provide a visible label for the combobox trigger in this pattern:
<Combobox.Root>
<Combobox.Label>Favorite fruit</Combobox.Label>
{/* ... */}
</Combobox.Root>
<Combobox.Label> renders a <div>, so clicking it focuses the combobox trigger without opening the popup.
Grouped
Organize related options with <Combobox.Group> and <Combobox.GroupLabel> to add section headings inside the popup.
Groups are represented by an array of objects with an items property, which itself is an array of individual items for each group. An extra property, such as value, can be provided for the heading text when rendering the group label.
interface ProduceGroupItem {
value: string;
items: string[];
}
const groups: ProduceGroupItem[] = [
{
value: 'Fruits',
items: ['Apple', 'Banana', 'Orange'],
},
{
value: 'Vegetables',
items: ['Carrot', 'Lettuce', 'Spinach'],
},
];
import { createUniqueId } from 'solid-js';
import type { JSX } from '@solidjs/web';
import { Combobox } from 'base-ui-solid/combobox';
import styles from './index.module.css';
export default function ExampleGroupedCombobox() {
const id = createUniqueId();
return (
<Combobox.Root items={groupedProduce}>
<div class={styles.Label}>
<label for={id}>Select produce</label>
<Combobox.InputGroup class={styles.InputGroup}>
<Combobox.Input placeholder="e.g. Mango" class={styles.Input} id={id} />
<div class={styles.ActionButtons}>
<Combobox.Clear class={styles.Clear} aria-label="Clear selection">
<XIcon />
</Combobox.Clear>
<Combobox.Trigger class={styles.Trigger} aria-label="Open popup">
<CaretDownIcon />
</Combobox.Trigger>
</div>
</Combobox.InputGroup>
</div>
<Combobox.Portal>
<Combobox.Positioner class={styles.Positioner} sideOffset={4}>
<Combobox.Popup class={styles.Popup}>
<Combobox.Empty>
<div class={styles.Empty}>No produce found.</div>
</Combobox.Empty>
<Combobox.List class={styles.List}>
{(group: ProduceGroup) => (
<Combobox.Group items={group.items} class={styles.Group}>
<Combobox.GroupLabel class={styles.GroupLabel}>{group.value}</Combobox.GroupLabel>
<Combobox.Collection>
{(item: Produce) => (
<Combobox.Item class={styles.Item} value={item}>
<Combobox.ItemIndicator class={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
<span class={styles.ItemText}>{item.label}</span>
</Combobox.Item>
)}
</Combobox.Collection>
</Combobox.Group>
)}
</Combobox.List>
</Combobox.Popup>
</Combobox.Positioner>
</Combobox.Portal>
</Combobox.Root>
);
}
function CheckIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="m2.5 8.5 4 4 7-9" />
</svg>
);
}
function XIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-linecap="square"
stroke-linejoin="round"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="m4.5 4.5 7 7m-7 0 7-7" />
</svg>
);
}
function CaretDownIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="currentColor"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="M12 6H4l4 4.5z" />
</svg>
);
}
interface Produce {
id: string;
label: string;
group: 'Fruits' | 'Vegetables';
}
interface ProduceGroup {
value: string;
items: Produce[];
}
const produceData: Produce[] = [
{ id: 'fruit-apple', label: 'Apple', group: 'Fruits' },
{ id: 'fruit-banana', label: 'Banana', group: 'Fruits' },
{ id: 'fruit-mango', label: 'Mango', group: 'Fruits' },
{ id: 'fruit-kiwi', label: 'Kiwi', group: 'Fruits' },
{ id: 'fruit-grape', label: 'Grape', group: 'Fruits' },
{ id: 'fruit-orange', label: 'Orange', group: 'Fruits' },
{ id: 'fruit-strawberry', label: 'Strawberry', group: 'Fruits' },
{ id: 'fruit-watermelon', label: 'Watermelon', group: 'Fruits' },
{ id: 'veg-broccoli', label: 'Broccoli', group: 'Vegetables' },
{ id: 'veg-carrot', label: 'Carrot', group: 'Vegetables' },
{ id: 'veg-cauliflower', label: 'Cauliflower', group: 'Vegetables' },
{ id: 'veg-cucumber', label: 'Cucumber', group: 'Vegetables' },
{ id: 'veg-kale', label: 'Kale', group: 'Vegetables' },
{ id: 'veg-pepper', label: 'Bell pepper', group: 'Vegetables' },
{ id: 'veg-spinach', label: 'Spinach', group: 'Vegetables' },
{ id: 'veg-zucchini', label: 'Zucchini', group: 'Vegetables' },
];
function groupProduce(items: Produce[]): ProduceGroup[] {
const groups: Record<string, Produce[]> = {};
items.forEach((item) => {
(groups[item.group] ??= []).push(item);
});
const order = ['Fruits', 'Vegetables'];
return order.map((value) => ({ value, items: groups[value] ?? [] }));
}
const groupedProduce: ProduceGroup[] = groupProduce(produceData);
Async search (single)
Load items from a remote source by fetching on input changes. Keep the selected item in the items list so it remains available while new results stream in. This pattern avoids needing to load items upfront.
import { createSignal, createMemo, createUniqueId } from 'solid-js';
import type { JSX } from '@solidjs/web';
import { Combobox } from 'base-ui-solid/combobox';
import styles from './index.module.css';
export default function ExampleAsyncSingleCombobox() {
const id = createUniqueId();
const [searchResults, setSearchResults] = createSignal<DirectoryUser[]>([]);
const [selectedValue, setSelectedValue] = createSignal<DirectoryUser | null>(null);
const [searchValue, setSearchValue] = createSignal('');
const [error, setError] = createSignal<string | null>(null);
const [isPending, setPending] = createSignal(false);
let pendingCount = 0;
async function startTransition(work: () => unknown) {
pendingCount += 1;
setPending(true);
try {
await work();
} finally {
pendingCount -= 1;
setPending(pendingCount > 0);
}
}
const { contains } = Combobox.useFilter();
const abortControllerRef = { current: null } as { current: AbortController | null };
const trimmedSearchValue = createMemo(() => searchValue().trim());
const items = createMemo(() => {
if (!selectedValue() || searchResults().some((user) => user.id === selectedValue()?.id)) {
return searchResults();
}
return [...searchResults(), selectedValue()];
});
function getStatus() {
if (isPending()) {
return (
<>
<span class={styles.Spinner} aria-hidden="true" />
Searching…
</>
);
}
if (error()) {
return error();
}
if (trimmedSearchValue() === '') {
return selectedValue() ? null : 'Start typing to search people…';
}
if (searchResults().length === 0) {
return `No matches for "${trimmedSearchValue()}".`;
}
return null;
}
function getEmptyMessage() {
if (trimmedSearchValue() === '' || isPending() || searchResults().length > 0 || error()) {
return null;
}
return 'Try a different search term.';
}
const status = createMemo(getStatus);
const emptyMessage = createMemo(() => getEmptyMessage());
return (
<Combobox.Root
items={items()}
itemToStringLabel={(user: DirectoryUser) => user.name}
isItemEqualToValue={(item, value) => item.id === value.id}
filter={null}
onOpenChangeComplete={(open) => {
if (!open && selectedValue()) {
setSearchResults([selectedValue()!]);
}
}}
onValueChange={(nextSelectedValue) => {
setSelectedValue(nextSelectedValue);
setSearchValue('');
setError(null);
}}
onInputValueChange={(nextSearchValue, { reason }) => {
setSearchValue(nextSearchValue);
const controller = new AbortController();
abortControllerRef.current?.abort();
abortControllerRef.current = controller;
if (nextSearchValue === '') {
setSearchResults([]);
setError(null);
return;
}
if (reason === 'item-press') {
return;
}
startTransition(async () => {
setError(null);
const result = await searchUsers(nextSearchValue, contains);
if (controller.signal.aborted) {
return;
}
startTransition(() => {
setSearchResults(result.users);
setError(result.error);
});
});
}}
>
<div class={styles.Label}>
<label for={id}>Assign reviewer</label>
<Combobox.InputGroup class={styles.InputGroup}>
<Combobox.Input id={id} placeholder="e.g. Michael" class={styles.Input} />
<div class={styles.ActionButtons}>
<Combobox.Clear class={styles.Clear} aria-label="Clear selection">
<XIcon />
</Combobox.Clear>
<Combobox.Trigger class={styles.Trigger} aria-label="Open popup">
<CaretDownIcon />
</Combobox.Trigger>
</div>
</Combobox.InputGroup>
</div>
<Combobox.Portal>
<Combobox.Positioner class={styles.Positioner} sideOffset={4}>
<Combobox.Popup class={styles.Popup} aria-busy={isPending() ? 'true' : undefined}>
<div class={styles.Viewport}>
<Combobox.Status>
{status() ? <div class={styles.Status}>{status()}</div> : null}
</Combobox.Status>
<Combobox.Empty>
{emptyMessage() ? <div class={styles.Empty}>{emptyMessage()}</div> : null}
</Combobox.Empty>
<Combobox.List>
{(user: DirectoryUser) => (
<Combobox.Item class={styles.Item} value={user}>
<Combobox.ItemIndicator class={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
<span class={styles.ItemText}>
<span class={styles.ItemTitle}>{user.name}</span>
<span class={styles.ItemEmail}>{user.email}</span>
<span class={styles.ItemSubtitle}>
<span>@{user.username}</span>
<span>{user.title}</span>
</span>
</span>
</Combobox.Item>
)}
</Combobox.List>
</div>
</Combobox.Popup>
</Combobox.Positioner>
</Combobox.Portal>
</Combobox.Root>
);
}
function CheckIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="m2.5 8.5 4 4 7-9" />
</svg>
);
}
function XIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-linecap="square"
stroke-linejoin="round"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="m4.5 4.5 7 7m-7 0 7-7" />
</svg>
);
}
function CaretDownIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="currentColor"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="M12 6H4l4 4.5z" />
</svg>
);
}
interface DirectoryUser {
id: string;
name: string;
username: string;
email: string;
title: string;
}
async function searchUsers(
query: string,
filter: (item: string, query: string) => boolean,
): Promise<{ users: DirectoryUser[]; error: string | null }> {
// Simulate network delay
await new Promise((resolve) => {
setTimeout(resolve, Math.random() * 500 + 100);
});
// Simulate occasional network errors (1% chance)
if (Math.random() < 0.01 || query === 'will_error') {
return {
users: [],
error: 'Failed to fetch people. Please try again.',
};
}
const users = allUsers.filter((user) => {
return (
filter(user.name, query) ||
filter(user.username, query) ||
filter(user.email, query) ||
filter(user.title, query)
);
});
return {
users,
error: null,
};
}
const allUsers: DirectoryUser[] = [
{
id: 'leslie-alexander',
name: 'Leslie Alexander',
username: 'leslie',
email: 'leslie.alexander@example.com',
title: 'Product Manager',
},
{
id: 'kathryn-murphy',
name: 'Kathryn Murphy',
username: 'kathryn',
email: 'kathryn.murphy@example.com',
title: 'Marketing Lead',
},
{
id: 'courtney-henry',
name: 'Courtney Henry',
username: 'courtney',
email: 'courtney.henry@example.com',
title: 'Design Systems',
},
{
id: 'michael-foster',
name: 'Michael Foster',
username: 'michael',
email: 'michael.foster@example.com',
title: 'Engineering Manager',
},
{
id: 'lindsay-walton',
name: 'Lindsay Walton',
username: 'lindsay',
email: 'lindsay.walton@example.com',
title: 'Product Designer',
},
{
id: 'tom-cook',
name: 'Tom Cook',
username: 'tom',
email: 'tom.cook@example.com',
title: 'Frontend Engineer',
},
{
id: 'whitney-francis',
name: 'Whitney Francis',
username: 'whitney',
email: 'whitney.francis@example.com',
title: 'Customer Success',
},
{
id: 'jacob-jones',
name: 'Jacob Jones',
username: 'jacob',
email: 'jacob.jones@example.com',
title: 'Security Engineer',
},
{
id: 'arlene-mccoy',
name: 'Arlene McCoy',
username: 'arlene',
email: 'arlene.mccoy@example.com',
title: 'Data Analyst',
},
{
id: 'marvin-mckinney',
name: 'Marvin McKinney',
username: 'marvin',
email: 'marvin.mckinney@example.com',
title: 'QA Specialist',
},
{
id: 'eleanor-pena',
name: 'Eleanor Pena',
username: 'eleanor',
email: 'eleanor.pena@example.com',
title: 'Operations',
},
{
id: 'jerome-bell',
name: 'Jerome Bell',
username: 'jerome',
email: 'jerome.bell@example.com',
title: 'DevOps Engineer',
},
];
Async search (multiple)
Load items from a remote source by fetching on input changes while supporting multiple selections. Selected items remain available in the list while new matches stream in. This pattern avoids needing to load items upfront.
import { createSignal, createMemo, createUniqueId, For } from 'solid-js';
import type { JSX } from '@solidjs/web';
import { Combobox } from 'base-ui-solid/combobox';
import styles from './index.module.css';
export default function ExampleAsyncMultipleCombobox() {
const id = createUniqueId();
const [searchResults, setSearchResults] = createSignal<DirectoryUser[]>([]);
const [selectedValues, setSelectedValues] = createSignal<DirectoryUser[]>([]);
const [searchValue, setSearchValue] = createSignal('');
const [error, setError] = createSignal<string | null>(null);
const [blockStartStatus, setBlockStartStatus] = createSignal(false);
const [isPending, setPending] = createSignal(false);
let pendingCount = 0;
async function startTransition(work: () => unknown) {
pendingCount += 1;
setPending(true);
try {
await work();
} finally {
pendingCount -= 1;
setPending(pendingCount > 0);
}
}
const { contains } = Combobox.useFilter();
const abortControllerRef = { current: null } as { current: AbortController | null };
const selectedValuesRef = { current: [] } as { current: DirectoryUser[] };
const trimmedSearchValue = createMemo(() => searchValue().trim());
const items = createMemo(() => {
if (selectedValues().length === 0) {
return searchResults();
}
const merged = [...searchResults()];
selectedValues().forEach((user) => {
if (!searchResults().some((result) => result.id === user.id)) {
merged.push(user);
}
});
return merged;
});
function getStatus() {
if (isPending()) {
return (
<>
<span class={styles.Spinner} aria-hidden="true" />
Searching…
</>
);
}
if (error()) {
return error();
}
if (trimmedSearchValue() === '' && !blockStartStatus()) {
return selectedValues().length > 0 ? null : 'Start typing to search people…';
}
if (searchResults().length === 0 && !blockStartStatus()) {
return `No matches for "${trimmedSearchValue()}".`;
}
return null;
}
function getEmptyMessage() {
if (trimmedSearchValue() === '' || isPending() || searchResults().length > 0 || error()) {
return null;
}
return 'Try a different search term.';
}
const status = createMemo(getStatus);
const emptyMessage = createMemo(() => getEmptyMessage());
return (
<Combobox.Root
items={items()}
itemToStringLabel={(user: DirectoryUser) => user.name}
isItemEqualToValue={(item, value) => item.id === value.id}
multiple
filter={null}
onOpenChangeComplete={(open) => {
if (!open) {
setSearchResults(selectedValuesRef.current);
setBlockStartStatus(false);
}
}}
onValueChange={(nextSelectedValues) => {
selectedValuesRef.current = nextSelectedValues;
setSelectedValues(nextSelectedValues);
setSearchValue('');
setError(null);
if (nextSelectedValues.length === 0) {
setSearchResults([]);
setBlockStartStatus(false);
} else {
setBlockStartStatus(true);
}
}}
onInputValueChange={(nextSearchValue, { reason }) => {
setSearchValue(nextSearchValue);
const controller = new AbortController();
abortControllerRef.current?.abort();
abortControllerRef.current = controller;
if (nextSearchValue === '') {
setSearchResults(selectedValuesRef.current);
setError(null);
setBlockStartStatus(false);
return;
}
if (reason === 'item-press') {
return;
}
startTransition(async () => {
setError(null);
const result = await searchUsers(nextSearchValue, contains);
if (controller.signal.aborted) {
return;
}
startTransition(() => {
setSearchResults(result.users);
setError(result.error);
});
});
}}
>
<div class={styles.Container}>
<label class={styles.Label} for={id}>
Assign reviewers
</label>
<Combobox.InputGroup class={styles.InputGroup}>
<Combobox.Value>
{(value: DirectoryUser[]) => (
<Combobox.Chips
class={styles.Chips}
aria-label={value.length > 0 ? 'Selected reviewers' : undefined}
>
<For each={value}>
{(user) => (
<Combobox.Chip
class={styles.Chip}
aria-label={user.name}
aria-description="Press Backspace or Delete to remove"
>
{user.name}
<Combobox.ChipRemove
class={styles.ChipRemove}
aria-label={`Remove ${user.name}`}
>
<XIcon />
</Combobox.ChipRemove>
</Combobox.Chip>
)}
</For>
<Combobox.Input
id={id}
placeholder={value.length > 0 ? '' : 'e.g. Michael'}
aria-description={
value.length > 0
? `${value.length} selected. From the start of the input, press Left Arrow to focus the selected items`
: undefined
}
class={styles.Input}
/>
</Combobox.Chips>
)}
</Combobox.Value>
</Combobox.InputGroup>
</div>
<Combobox.Portal>
<Combobox.Positioner class={styles.Positioner} sideOffset={4}>
<Combobox.Popup class={styles.Popup} aria-busy={isPending() ? 'true' : undefined}>
<div class={styles.Viewport}>
<Combobox.Status>
{status() ? <div class={styles.Status}>{status()}</div> : null}
</Combobox.Status>
<Combobox.Empty>
{emptyMessage() ? <div class={styles.Empty}>{emptyMessage()}</div> : null}
</Combobox.Empty>
<Combobox.List>
{(user: DirectoryUser) => (
<Combobox.Item class={styles.Item} value={user}>
<Combobox.ItemIndicator class={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
<span class={styles.ItemText}>
<span class={styles.ItemTitle}>{user.name}</span>
<span class={styles.ItemEmail}>{user.email}</span>
<span class={styles.ItemSubtitle}>
<span>@{user.username}</span>
<span>{user.title}</span>
</span>
</span>
</Combobox.Item>
)}
</Combobox.List>
</div>
</Combobox.Popup>
</Combobox.Positioner>
</Combobox.Portal>
</Combobox.Root>
);
}
function CheckIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="m2.5 8.5 4 4 7-9" />
</svg>
);
}
function XIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-linecap="square"
stroke-linejoin="round"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="m4.5 4.5 7 7m-7 0 7-7" />
</svg>
);
}
interface DirectoryUser {
id: string;
name: string;
username: string;
email: string;
title: string;
}
async function searchUsers(
query: string,
filter: (item: string, query: string) => boolean,
): Promise<{ users: DirectoryUser[]; error: string | null }> {
// Simulate network delay
await new Promise((resolve) => {
setTimeout(resolve, Math.random() * 500 + 100);
});
// Simulate occasional network errors (1% chance)
if (Math.random() < 0.01 || query === 'will_error') {
return {
users: [],
error: 'Failed to fetch people. Please try again.',
};
}
const users = allUsers.filter((user) => {
return (
filter(user.name, query) ||
filter(user.username, query) ||
filter(user.email, query) ||
filter(user.title, query)
);
});
return {
users,
error: null,
};
}
const allUsers: DirectoryUser[] = [
{
id: 'leslie-alexander',
name: 'Leslie Alexander',
username: 'leslie',
email: 'leslie.alexander@example.com',
title: 'Product Manager',
},
{
id: 'kathryn-murphy',
name: 'Kathryn Murphy',
username: 'kathryn',
email: 'kathryn.murphy@example.com',
title: 'Marketing Lead',
},
{
id: 'courtney-henry',
name: 'Courtney Henry',
username: 'courtney',
email: 'courtney.henry@example.com',
title: 'Design Systems',
},
{
id: 'michael-foster',
name: 'Michael Foster',
username: 'michael',
email: 'michael.foster@example.com',
title: 'Engineering Manager',
},
{
id: 'lindsay-walton',
name: 'Lindsay Walton',
username: 'lindsay',
email: 'lindsay.walton@example.com',
title: 'Product Designer',
},
{
id: 'tom-cook',
name: 'Tom Cook',
username: 'tom',
email: 'tom.cook@example.com',
title: 'Frontend Engineer',
},
{
id: 'whitney-francis',
name: 'Whitney Francis',
username: 'whitney',
email: 'whitney.francis@example.com',
title: 'Customer Success',
},
{
id: 'jacob-jones',
name: 'Jacob Jones',
username: 'jacob',
email: 'jacob.jones@example.com',
title: 'Security Engineer',
},
{
id: 'arlene-mccoy',
name: 'Arlene McCoy',
username: 'arlene',
email: 'arlene.mccoy@example.com',
title: 'Data Analyst',
},
{
id: 'marvin-mckinney',
name: 'Marvin McKinney',
username: 'marvin',
email: 'marvin.mckinney@example.com',
title: 'QA Specialist',
},
{
id: 'eleanor-pena',
name: 'Eleanor Pena',
username: 'eleanor',
email: 'eleanor.pena@example.com',
title: 'Operations',
},
{
id: 'jerome-bell',
name: 'Jerome Bell',
username: 'jerome',
email: 'jerome.bell@example.com',
title: 'DevOps Engineer',
},
];
Creatable
Create a new item when the filter matches no items, opening a creation <Dialog>.
import { createSignal, createMemo, createUniqueId, For } from 'solid-js';
import type { JSX } from '@solidjs/web';
import { Combobox } from 'base-ui-solid/combobox';
import { Dialog } from 'base-ui-solid/dialog';
import styles from './index.module.css';
export default function ExampleCreatableCombobox() {
const id = createUniqueId();
const [labels, setLabels] = createSignal<LabelItem[]>(initialLabels);
const [selected, setSelected] = createSignal<LabelItem[]>([]);
const [query, setQuery] = createSignal('');
const [openDialog, setOpenDialog] = createSignal(false);
const createInputRef = { current: null } as { current: HTMLInputElement | null };
const comboboxInputRef = { current: null } as { current: HTMLInputElement | null };
const pendingQueryRef = { current: '' };
const highlightedItemRef = { current: undefined } as { current: LabelItem | undefined };
function handleInputKeyDown(event: KeyboardEvent) {
if (event.key !== 'Enter' || highlightedItemRef.current) {
return;
}
const currentTrimmed = query().trim();
if (currentTrimmed === '') {
return;
}
const normalized = currentTrimmed.toLocaleLowerCase();
const existing = labels().find(
(label) => label.value.trim().toLocaleLowerCase() === normalized,
);
if (existing) {
setSelected((prev) =>
prev.some((item) => item.id === existing.id) ? prev : [...prev, existing],
);
setQuery('');
return;
}
pendingQueryRef.current = currentTrimmed;
setOpenDialog(true);
}
function handleCreate() {
const input = createInputRef.current || comboboxInputRef.current;
const value = input ? input.value.trim() : '';
if (!value) {
return;
}
const normalized = value.toLocaleLowerCase();
const baseId = normalized.replace(/\s+/g, '-');
const existing = labels().find((l) => l.value.trim().toLocaleLowerCase() === normalized);
if (existing) {
setSelected((prev) => (prev.some((i) => i.id === existing.id) ? prev : [...prev, existing]));
setOpenDialog(false);
setQuery('');
return;
}
// Ensure we don't collide with an existing id (e.g., value "docs" vs. existing id "docs")
const existingIds = new Set(labels().map((l) => l.id));
let uniqueId = baseId;
if (existingIds.has(uniqueId)) {
let i = 2;
while (existingIds.has(`${baseId}-${i}`)) {
i += 1;
}
uniqueId = `${baseId}-${i}`;
}
const newItem: LabelItem = { id: uniqueId, value };
if (!selected().find((item) => item.id === newItem.id)) {
setLabels((prev) => [...prev, newItem]);
setSelected((prev) => [...prev, newItem]);
}
setOpenDialog(false);
setQuery('');
}
function handleCreateSubmit(event: SubmitEvent) {
event.preventDefault();
handleCreate();
}
const trimmed = createMemo(() => query().trim());
const lowered = createMemo(() => trimmed().toLocaleLowerCase());
const exactExists = createMemo(() =>
labels().some((l) => l.value.trim().toLocaleLowerCase() === lowered()),
);
// Show the creatable item alongside matches if there's no exact match
const itemsForView = createMemo(() =>
trimmed() !== '' && !exactExists()
? [
...labels(),
{ creatable: trimmed(), id: `create:${lowered()}`, value: `Create "${trimmed()}"` },
]
: labels(),
);
return (
<>
<Combobox.Root
items={itemsForView()}
multiple
onValueChange={(next) => {
const creatableSelection = next.find(
(item) => item.creatable && !selected().some((current) => current.id === item.id),
);
if (creatableSelection && creatableSelection.creatable) {
pendingQueryRef.current = creatableSelection.creatable;
setOpenDialog(true);
return;
}
const clean = next.filter((i) => !i.creatable);
setSelected(clean);
setQuery('');
}}
value={selected()}
inputValue={query()}
onInputValueChange={setQuery}
onItemHighlighted={(item) => {
highlightedItemRef.current = item;
}}
>
<div class={styles.Container}>
<label class={styles.Label} for={id}>
Labels
</label>
<Combobox.InputGroup class={styles.InputGroup}>
<Combobox.Value>
{(value: LabelItem[]) => (
<Combobox.Chips
class={styles.Chips}
aria-label={value.length > 0 ? 'Selected labels' : undefined}
>
<For each={value}>
{(label) => (
<Combobox.Chip
class={styles.Chip}
aria-label={label.value}
aria-description="Press Backspace or Delete to remove"
>
{label.value}
<Combobox.ChipRemove
class={styles.ChipRemove}
aria-label={`Remove ${label.value}`}
>
<XIcon />
</Combobox.ChipRemove>
</Combobox.Chip>
)}
</For>
<Combobox.Input
ref={(element) => {
comboboxInputRef.current = element;
}}
id={id}
placeholder={value.length > 0 ? '' : 'e.g. bug'}
aria-description={
value.length > 0
? `${value.length} selected. From the start of the input, press Left Arrow to focus the selected items`
: undefined
}
class={styles.Input}
onKeyDown={handleInputKeyDown}
/>
</Combobox.Chips>
)}
</Combobox.Value>
</Combobox.InputGroup>
</div>
<Combobox.Portal>
<Combobox.Positioner class={styles.Positioner} sideOffset={4}>
<Combobox.Popup class={styles.Popup}>
<Combobox.Empty>
<div class={styles.Empty}>No labels found.</div>
</Combobox.Empty>
<Combobox.List>
{(item: LabelItem) =>
item.creatable ? (
<Combobox.Item class={styles.Item} value={item}>
<span class={styles.ItemIndicator}>
<PlusIcon />
</span>
<span class={styles.ItemText}>Create "{item.creatable}"</span>
</Combobox.Item>
) : (
<Combobox.Item class={styles.Item} value={item}>
<Combobox.ItemIndicator class={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
<span class={styles.ItemText}>{item.value}</span>
</Combobox.Item>
)
}
</Combobox.List>
</Combobox.Popup>
</Combobox.Positioner>
</Combobox.Portal>
</Combobox.Root>
<Dialog.Root open={openDialog()} onOpenChange={setOpenDialog}>
<Dialog.Portal>
<Dialog.Backdrop class={styles.Backdrop} />
<Dialog.Popup class={styles.DialogPopup} initialFocus={() => createInputRef.current}>
<Dialog.Title class={styles.Title}>Create new label</Dialog.Title>
<Dialog.Description class={styles.Description}>
Add a new label to select.
</Dialog.Description>
<form onSubmit={handleCreateSubmit}>
<input
ref={(element) => {
createInputRef.current = element;
}}
class={styles.TextField}
placeholder="Label name"
defaultValue={pendingQueryRef.current}
/>
<div class={styles.Actions}>
<Dialog.Close class={styles.Button}>Cancel</Dialog.Close>
<button type="submit" class={styles.Button}>
Create
</button>
</div>
</form>
</Dialog.Popup>
</Dialog.Portal>
</Dialog.Root>
</>
);
}
function CheckIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="m2.5 8.5 4 4 7-9" />
</svg>
);
}
function PlusIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-linecap="square"
stroke-linejoin="round"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="M1.5 8h13M8 14.5v-13" />
</svg>
);
}
function XIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-linecap="square"
stroke-linejoin="round"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="m4.5 4.5 7 7m-7 0 7-7" />
</svg>
);
}
interface LabelItem {
creatable?: string;
id: string;
value: string;
}
const initialLabels: LabelItem[] = [
{ id: 'bug', value: 'bug' },
{ id: 'docs', value: 'documentation' },
{ id: 'enhancement', value: 'enhancement' },
{ id: 'help-wanted', value: 'help wanted' },
{ id: 'good-first-issue', value: 'good first issue' },
];
Virtualized
Efficiently handle large datasets using a virtualization library like @tanstack/virtual-core.
import { onSettled, For } from 'solid-js';
import type { JSX } from '@solidjs/web';
import { Combobox } from 'base-ui-solid/combobox';
import { useVirtualizer } from './useVirtualizer';
import styles from './index.module.css';
export default function ExampleVirtualizedCombobox() {
const virtualizerRef = { current: null } as { current: Virtualizer | null };
return (
<Combobox.Root
virtualized
items={virtualizedItems}
itemToStringLabel={getItemLabel}
onItemHighlighted={(item, { reason, index }) => {
const virtualizer = virtualizerRef.current;
if (!item || !virtualizer) {
return;
}
const isStart = index === 0;
const isEnd = index === virtualizer.options.count - 1;
// `imperative-action` can jump anywhere in the list, so it always needs a scroll:
// unlike the arrow keys it can target an item that is not currently rendered.
const shouldScroll =
reason === 'none' ||
reason === 'imperative-action' ||
(reason === 'keyboard' && (isStart || isEnd));
if (shouldScroll) {
queueMicrotask(() => {
virtualizer.scrollToIndex(index, { align: isEnd ? 'start' : 'end' });
});
}
}}
>
<label class={styles.Label}>
Search 10,000 items
<Combobox.Input class={styles.Input} />
</label>
<Combobox.Portal>
<Combobox.Positioner class={styles.Positioner} sideOffset={4}>
<Combobox.Popup class={styles.Popup}>
<Combobox.Empty>
<div class={styles.Empty}>No items found.</div>
</Combobox.Empty>
<Combobox.List class={styles.List}>
<VirtualizedList virtualizerRef={virtualizerRef} />
</Combobox.List>
</Combobox.Popup>
</Combobox.Positioner>
</Combobox.Portal>
</Combobox.Root>
);
}
function VirtualizedList(props: { virtualizerRef: { current: Virtualizer | null } }) {
const filteredItems = Combobox.useFilteredItems<VirtualizedItem>();
const scrollElementRef = { current: null } as { current: HTMLDivElement | null };
const virtualizer = useVirtualizer({
get count() {
return filteredItems().length;
},
getScrollElement: () => scrollElementRef.current,
estimateSize: () => 32,
overscan: 20,
paddingStart: 4,
paddingEnd: 4,
scrollPaddingEnd: 4,
scrollPaddingStart: 4,
});
onSettled(() => {
props.virtualizerRef.current = virtualizer;
return () => {
props.virtualizerRef.current = null;
};
});
const handleScrollElementRef = (element: HTMLDivElement | null) => {
scrollElementRef.current = element;
if (element) {
virtualizer.measure();
}
};
const totalSize = () => virtualizer.getTotalSize();
return (
<div
role="presentation"
ref={handleScrollElementRef}
class={styles.Scroller}
style={{ '--total-size': `${totalSize()}px` } as JSX.CSSProperties}
>
<div
role="presentation"
class={styles.VirtualizedPlaceholder}
style={{ height: `${totalSize()}px` }}
>
<For each={virtualizer.getVirtualItems()}>
{(virtualItem) => {
const item = filteredItems()[virtualItem.index];
if (!item) {
return null;
}
return (
<Combobox.Item
index={virtualItem.index}
data-index={virtualItem.index}
ref={virtualizer.measureElement}
value={item}
class={styles.Item}
aria-setsize={filteredItems().length}
aria-posinset={virtualItem.index + 1}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<Combobox.ItemIndicator class={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
<span class={styles.ItemText}>{item.name}</span>
</Combobox.Item>
);
}}
</For>
</div>
</div>
);
}
function CheckIcon(props: JSX.IntrinsicElements['svg']) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
{...props}
style={
typeof props.style === 'string'
? `display: block; ${props.style}`
: { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
}
>
<path d="m2.5 8.5 4 4 7-9" />
</svg>
);
}
interface VirtualizedItem {
id: string;
name: string;
}
function getItemLabel(item: VirtualizedItem | null) {
return item ? item.name : '';
}
const virtualizedItems: VirtualizedItem[] = Array.from({ length: 10000 }, (_, index) => {
const id = String(index + 1);
const indexLabel = id.padStart(4, '0');
return { id, name: `Item ${indexLabel}` };
});
type Virtualizer = ReturnType<typeof useVirtualizer<HTMLDivElement, Element>>;
When using highlightItem(), scroll your virtualizer to the index reported by onItemHighlighted for the 'imperative-action' reason. The highlighted item may not be rendered yet.
Memoizing items
Solid doesn’t need memoized items. Each item component runs once, and typing only mounts or unmounts the items whose filter result changes, so unchanged items never re-render. Rendering a large list still costs time when the popup opens. Use virtualization once that becomes noticeable on low-end devices.
Custom keyboard shortcuts
Use actionsRef.highlightItem() to navigate the open list with custom keyboard shortcuts, as shown in the Autocomplete example.
API reference
Root
Groups all parts of the combobox. Doesn’t render its own HTML element.
namestring—
stringdefaultValueUnion—
value prop instead.Value[] | Value | nullvalueUnion—
Value[] | Value | nullonValueChangefunction—
((value: Value[] | Value | null, eventDetails: Combobox.Root.ChangeEventDetails) => void)defaultInputValueUnion—
inputValue prop instead.string | number | string[]inputValueUnion—
string | string[] | numberonInputValueChangefunction—
((inputValue: string, eventDetails: Combobox.Root.ChangeEventDetails) => void)defaultOpenbooleanfalse
open prop instead.booleanopenboolean—
booleanonOpenChangefunction—
((open: boolean, eventDetails: Combobox.Root.OpenChangeEventDetails) => void)autoHighlightbooleanfalse
booleanhighlightItemOnHoverbooleantrue
:hover to be differentiated from the :focus (data-highlighted) state.booleanactionsRefRefObject<Combobox.Root.Actions | null>—
unmount: Ends the closing phase of the combobox after an externally controlled closing animation finishes.
Call preventUnmountOnClose() in onOpenChange first, otherwise the combobox completes closing on its own.
Whether it leaves the DOM is decided by keepMounted on the portal.close: Closes the combobox imperatively when called.highlightItem: Moves or clears the highlight while the popup is open.
'next' and 'previous' move sequentially through the items, including across rows in a
grid, and wrap when loopFocus is enabled. Unlike the arrow keys, they never return the
highlight to the input. 'first' and 'last' highlight the first or last item.
'none' clears the highlight.
Calling this action does not open the popup. To highlight an item after opening it, call
the action from onOpenChangeComplete when open is true.
Highlight changes requested through this action report the reason 'imperative-action'
to onItemHighlighted.RefObject<Combobox.Root.Actions | null>autoCompletestring—
stringfilterfunction—
items is a createItems()
collection, and the item itself otherwise.((item: Item, query: string, itemToString?: ((item: Item) => string)) => boolean) | nullfilteredItemsUnion—
items prop internally.
When items is also provided, this array must preserve its flat or grouped structure.
With a createItems() collection, pass source items rather than derived values.
Nullish entries are not supported, as in items.
Use when you want to control filtering logic externally with the useFilter() hook.Item[] | Group<Item>[]formstring—
stringgridbooleanfalse
booleaninlinebooleanfalse
open unconditionally in conjunction with this prop so the list is considered
visible: <Combobox.Root inline open> In a Combobox.Root > Dialog.Root composition, bind the Combobox’s open and
onOpenChange props to the Dialog's open and onOpenChange state instead so the
component resets its transient state (filter query, highlighted item, and input value) when
the dialog closes.booleanisItemEqualToValuefunction—
createItems() collection, both arguments are derived values.
Defaults to Object.is comparison.((itemValue: Value, value: Value) => boolean)itemToStringLabelfunction—
<Combobox.Item value={object}>), this function converts the object value to a string representation for display in the input.
If the shape of the object is { value, label }, the label will be used automatically without needing to specify this prop.
With a createItems() collection, this receives the derived value, and the collection’s
getLabel takes precedence for values it can resolve.((itemValue: Value) => string)itemToStringValuefunction—
<Combobox.Item value={object}>), this function converts the object value to a string representation for form submission.
If the shape of the object is { value, label }, the value will be used automatically without needing to specify this prop.
With a createItems() collection, this receives the derived value.((itemValue: Value) => string)itemsUnion—
createItems() function, which derives each item’s selection value and label.
Nullish entries are not supported: remove them from the data before passing it.any[] | Group[] | ComboboxItemCollection<Item, Value>limitnumber-1
numberlocaleIntl.LocalesArgument—
Intl.LocalesArgumentloopFocusbooleantrue
booleanmodalbooleanfalse
true: user interaction is limited to the popup: document page scroll is locked and pointer interactions on outside elements are disabled.false: user interaction with the rest of the document is allowed. On touch devices, a true modal blocks outside taps but leaves the page scrollable unless the popup spans nearly the full viewport width, matching native iOS behavior.booleanmultiplebooleanfalse
booleanonItemHighlightedfunction—
undefined if no item is highlighted) and event details with a reason property describing why the highlight changed.
The reason can be: 'keyboard': the highlight changed due to keyboard navigation.'pointer': the highlight changed due to pointer hovering. The event may be a MouseEvent
rather than a PointerEvent.'imperative-action': the highlight changed via actionsRef's highlightItem.'none': the highlight changed for another reason, such as typing, autoHighlight, the
item list changing, or the popup opening or closing.((highlightedValue: Value | undefined, eventDetails: Combobox.Root.HighlightEventDetails) => void)onOpenChangeCompletefunction—
((open: boolean) => void)openOnInputClickbooleantrue
booleanvirtualizedbooleanfalse
booleandisabledbooleanfalse
booleanreadOnlybooleanfalse
booleanrequiredbooleanfalse
booleaninputRefJSX.Ref<HTMLInputElement>—
JSX.Ref<HTMLInputElement>idstring—
stringchildrenJSX.Element—
JSX.ElementRoot.State
type ComboboxRootState = {};Root.Actions
type ComboboxRootActions = {
unmount: () => void;
close: () => void;
highlightItem: (target: Combobox.Root.HighlightItemTarget) => void;
};Root.ChangeEventReason
type ComboboxRootChangeEventReason =
| 'trigger-press'
| 'input-press'
| 'outside-press'
| 'item-press'
| 'close-press'
| 'escape-key'
| 'list-navigation'
| 'focus-out'
| 'input-change'
| 'input-clear'
| 'clear-press'
| 'chip-remove-press'
| 'cancel-open'
| 'imperative-action'
| 'none';Root.ChangeEventDetails
type ComboboxRootChangeEventDetails = (
| { reason: 'trigger-press'; event: MouseEvent | PointerEvent | TouchEvent | KeyboardEvent }
| { reason: 'input-press'; event: MouseEvent | PointerEvent | TouchEvent | KeyboardEvent }
| { reason: 'outside-press'; event: MouseEvent | PointerEvent | TouchEvent }
| { reason: 'item-press'; event: MouseEvent | PointerEvent | KeyboardEvent }
| { reason: 'close-press'; event: MouseEvent | PointerEvent | KeyboardEvent }
| { reason: 'escape-key'; event: KeyboardEvent }
| { reason: 'list-navigation'; event: KeyboardEvent }
| { reason: 'focus-out'; event: KeyboardEvent | FocusEvent }
| { reason: 'input-change'; event: Event | InputEvent }
| { reason: 'input-clear'; event: Event | FocusEvent | InputEvent }
| { reason: 'clear-press'; event: MouseEvent | PointerEvent | KeyboardEvent }
| { reason: 'chip-remove-press'; event: MouseEvent | PointerEvent | KeyboardEvent }
| { reason: 'cancel-open'; event: MouseEvent }
| { reason: 'imperative-action'; event: Event }
| { reason: 'none'; event: Event }
) & {
/** Cancels Base UI from handling the event. */
cancel: () => void;
/** Allows the event to propagate in cases where Base UI will stop the propagation. */
allowPropagation: () => void;
/** Indicates whether the event has been canceled. */
isCanceled: boolean;
/** Indicates whether the event is allowed to propagate. */
isPropagationAllowed: boolean;
/** The element that triggered the event, if applicable. */
trigger: Element | undefined;
/**
* When `reason` is `input-clear` in multiple mode, indicates whether an item press caused the
* clear. Automatic cleanup clears omit this property.
*/
isItemPress?: boolean;
};Root.HighlightEventReason
type ComboboxRootHighlightEventReason = 'keyboard' | 'pointer' | 'imperative-action' | 'none';Root.HighlightEventDetails
type ComboboxRootHighlightEventDetails =
| { reason: 'imperative-action'; event: Event; index: number }
| { reason: 'none'; event: Event; index: number }
| { reason: 'pointer'; event: MouseEvent | PointerEvent; index: number }
| { reason: 'keyboard'; event: KeyboardEvent; index: number };Root.HighlightItemTarget
type ComboboxRootHighlightItemTarget = 'next' | 'previous' | 'first' | 'last' | 'none';Root.OpenChangeEventDetails
type ComboboxRootOpenChangeEventDetails = (
| { reason: 'trigger-press'; event: MouseEvent | PointerEvent | TouchEvent | KeyboardEvent }
| { reason: 'input-press'; event: MouseEvent | PointerEvent | TouchEvent | KeyboardEvent }
| { reason: 'outside-press'; event: MouseEvent | PointerEvent | TouchEvent }
| { reason: 'item-press'; event: MouseEvent | PointerEvent | KeyboardEvent }
| { reason: 'close-press'; event: MouseEvent | PointerEvent | KeyboardEvent }
| { reason: 'escape-key'; event: KeyboardEvent }
| { reason: 'list-navigation'; event: KeyboardEvent }
| { reason: 'focus-out'; event: KeyboardEvent | FocusEvent }
| { reason: 'input-change'; event: Event | InputEvent }
| { reason: 'input-clear'; event: Event | FocusEvent | InputEvent }
| { reason: 'clear-press'; event: MouseEvent | PointerEvent | KeyboardEvent }
| { reason: 'chip-remove-press'; event: MouseEvent | PointerEvent | KeyboardEvent }
| { reason: 'cancel-open'; event: MouseEvent }
| { reason: 'imperative-action'; event: Event }
| { reason: 'none'; event: Event }
) & {
/** Cancels Base UI from handling the event. */
cancel: () => void;
/** Allows the event to propagate in cases where Base UI will stop the propagation. */
allowPropagation: () => void;
/** Indicates whether the event has been canceled. */
isCanceled: boolean;
/** Indicates whether the event is allowed to propagate. */
isPropagationAllowed: boolean;
/** The element that triggered the event, if applicable. */
trigger: Element | undefined;
/**
* When `reason` is `input-clear` in multiple mode, indicates whether an item press caused the
* clear. Automatic cleanup clears omit this property.
*/
isItemPress?: boolean;
/** Prevents the popup from unmounting until the `unmount` action is called. */
preventUnmountOnClose: () => void;
};Label
An accessible label that is automatically associated with the combobox trigger.
Renders a <div> element.
classfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Label.State
type ComboboxLabelState = {
/** Whether the component should ignore user interaction. */
disabled: boolean;
/** Whether the field has been touched. */
touched: boolean;
/** Whether the field value has changed from its initial value. */
dirty: boolean;
/** Whether the field is valid. */
valid: boolean | null;
/** Whether the field has a value. */
filled: boolean;
/** Whether the field is focused. */
focused: boolean;
};Value
The current value of the combobox. Doesn’t render its own HTML element.
placeholderJSX.Element—
children if specified, or by a null item’s label in items.JSX.Elementchildrenfunction—
JSX.Element to format the selected value.
Treat the value as read-only: in multiple mode it may be a shared frozen array
when nothing is selected.JSX.Element | ((selectedValue: any) => JSX.Element)Value.State
type ComboboxValueState = {};Icon
An icon that indicates that the trigger button opens the popup.
Renders a <span> element.
classfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Icon.State
type ComboboxIconState = {};Input
A text input to search for items in the list.
Renders an <input> element.
disabledbooleanfalse
booleanclassfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Data attributes
data-popup-open-—
-data-popup-sideUnion—
'top' | 'bottom' | 'left' | 'right' | 'inline-end' | 'inline-start' | nulldata-list-empty-—
-data-pressed-—
-data-disabled-—
-data-readonly-—
-data-required-—
-data-valid-—
-data-invalid-—
-data-dirty-—
-data-touched-—
-data-filled-—
-data-focused-—
-Attribute | Description | |
|---|---|---|
data-popup-open | Present when the corresponding popup is open. | |
data-popup-side | Indicates which side the corresponding popup is positioned relative to its anchor. | |
data-list-empty | Present when the corresponding items list is empty. | |
data-pressed | Present when the input is pressed. | |
data-disabled | Present when the component is disabled. | |
data-readonly | Present when the component is readonly. | |
data-required | Present when the component is required. | |
data-valid | Present when the component is in a valid state (when wrapped in Field.Root). | |
data-invalid | Present when the component is in an invalid state (when wrapped in Field.Root). | |
data-dirty | Present when the component’s value has changed (when wrapped in Field.Root). | |
data-touched | Present when the component has been touched (when wrapped in Field.Root). | |
data-filled | Present when the component has a value (when wrapped in Field.Root). | |
data-focused | Present when the input is focused (when wrapped in Field.Root). | |
Input.State
type ComboboxInputState = {
/** Whether the corresponding popup is open. */
open: boolean;
/** Indicates which side the corresponding popup is positioned relative to its anchor. */
popupSide: Side | null;
/** Present when the corresponding items list is empty. */
listEmpty: boolean;
/** Whether the component should ignore user edits. */
readOnly: boolean;
/** Whether the component should ignore user interaction. */
disabled: boolean;
/** Whether the field has been touched. */
touched: boolean;
/** Whether the field value has changed from its initial value. */
dirty: boolean;
/** Whether the field is valid. */
valid: boolean | null;
/** Whether the field has a value. */
filled: boolean;
/** Whether the field is focused. */
focused: boolean;
};InputGroup
A wrapper for the input and its associated controls.
Renders a <div> element.
classfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Data attributes
data-popup-open-—
-data-popup-sideUnion—
'top' | 'bottom' | 'left' | 'right' | 'inline-end' | 'inline-start' | nulldata-list-empty-—
-data-pressed-—
-data-disabled-—
-data-readonly-—
-data-valid-—
-data-invalid-—
-data-dirty-—
-data-touched-—
-data-filled-—
-data-focused-—
-data-placeholder-—
-Attribute | Description | |
|---|---|---|
data-popup-open | Present when the corresponding popup is open. | |
data-popup-side | Indicates which side the corresponding popup is positioned relative to its anchor. | |
data-list-empty | Present when the corresponding items list is empty. | |
data-pressed | Present when the input group is pressed. | |
data-disabled | Present when the component is disabled. | |
data-readonly | Present when the component is readonly. | |
data-valid | Present when the component is in a valid state (when wrapped in Field.Root). | |
data-invalid | Present when the component is in an invalid state (when wrapped in Field.Root). | |
data-dirty | Present when the component’s value has changed (when wrapped in Field.Root). | |
data-touched | Present when the component has been touched (when wrapped in Field.Root). | |
data-filled | Present when the component has a value (when wrapped in Field.Root). | |
data-focused | Present when the component is focused (when wrapped in Field.Root). | |
data-placeholder | Present when the combobox doesn’t have a value. | |
InputGroup.State
type ComboboxInputGroupState = {
/** Whether the corresponding popup is open. */
open: boolean;
/** Whether the component should ignore user interaction. */
disabled: boolean;
/** Whether the component should ignore user edits. */
readOnly: boolean;
/** Indicates which side the corresponding popup is positioned relative to its anchor. */
popupSide: Side | null;
/** Present when the corresponding items list is empty. */
listEmpty: boolean;
/** Whether the combobox doesn't have a value. */
placeholder: boolean;
/** Whether the field has been touched. */
touched: boolean;
/** Whether the field value has changed from its initial value. */
dirty: boolean;
/** Whether the field is valid. */
valid: boolean | null;
/** Whether the field has a value. */
filled: boolean;
/** Whether the field is focused. */
focused: boolean;
};Clear
Clears the value when clicked.
Renders a <button> element.
nativeButtonbooleantrue
<button> element when replacing it
via the render prop.
Set to false if the rendered element is not a button (for example, <div>).booleandisabledbooleanfalse
booleanclassfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)keepMountedbooleanfalse
booleanrenderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Data attributes
data-popup-open-—
-data-disabled-—
-data-visible-—
-data-starting-style-—
-data-ending-style-—
-Attribute | Description | |
|---|---|---|
data-popup-open | Present when the corresponding popup is open. | |
data-disabled | Present when the button is disabled. | |
data-visible | Present when the clear button is visible. | |
data-starting-style | Present when the button begins animating in. | |
data-ending-style | Present when the button is animating out. | |
Clear.State
type ComboboxClearState = {
/** Whether the popup is open. */
open: boolean;
/** Whether the component should ignore user interaction. */
disabled: boolean;
/** Whether the clear button should be visible. */
visible: boolean;
/** The transition status of the component. */
transitionStatus: TransitionStatus;
};Trigger
A button that opens the popup.
Renders a <button> element.
nativeButtonbooleantrue
<button> element when replacing it
via the render prop.
Set to false if the rendered element is not a button (for example, <div>).booleandisabledbooleanfalse
booleanclassfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Data attributes
data-popup-open-—
-data-popup-sideUnion—
'top' | 'bottom' | 'left' | 'right' | 'inline-end' | 'inline-start' | nulldata-list-empty-—
-data-pressed-—
-data-disabled-—
-data-readonly-—
-data-required-—
-data-valid-—
-data-invalid-—
-data-dirty-—
-data-touched-—
-data-filled-—
-data-focused-—
-data-placeholder-—
-Attribute | Description | |
|---|---|---|
data-popup-open | Present when the corresponding popup is open. | |
data-popup-side | Indicates which side the corresponding popup is positioned relative to its anchor. | |
data-list-empty | Present when the corresponding items list is empty. | |
data-pressed | Present when the trigger is pressed. | |
data-disabled | Present when the component is disabled. | |
data-readonly | Present when the component is readonly. | |
data-required | Present when the component is required. | |
data-valid | Present when the component is in a valid state (when wrapped in Field.Root). | |
data-invalid | Present when the component is in an invalid state (when wrapped in Field.Root). | |
data-dirty | Present when the component’s value has changed (when wrapped in Field.Root). | |
data-touched | Present when the component has been touched (when wrapped in Field.Root). | |
data-filled | Present when the component has a value (when wrapped in Field.Root). | |
data-focused | Present when the trigger is focused (when wrapped in Field.Root). | |
data-placeholder | Present when the combobox doesn’t have a value. | |
Trigger.State
type ComboboxTriggerState = {
/** Whether the popup is open. */
open: boolean;
/** Whether the component should ignore user interaction. */
disabled: boolean;
/** Whether the component should ignore user edits. */
readOnly: boolean;
/** Indicates which side the corresponding popup is positioned relative to its anchor. */
popupSide: Side | null;
/** Present when the corresponding items list is empty. */
listEmpty: boolean;
/** Whether the combobox doesn't have a value. */
placeholder: boolean;
/** Whether the field has been touched. */
touched: boolean;
/** Whether the field value has changed from its initial value. */
dirty: boolean;
/** Whether the field is valid. */
valid: boolean | null;
/** Whether the field has a value. */
filled: boolean;
/** Whether the field is focused. */
focused: boolean;
};Chips
A container for the chips in a multiselectable input.
Renders a <div> element.
classfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Chips.State
type ComboboxChipsState = {};Chip
An individual chip that represents a value in a multiselectable input.
Renders a <div> element.
classfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Chip.State
type ComboboxChipState = {
/** Whether the component should ignore user interaction. */
disabled: boolean;
};ChipRemove
A button to remove a chip.
Renders a <button> element.
nativeButtonbooleantrue
<button> element when replacing it
via the render prop.
Set to false if the rendered element is not a button (for example, <div>).booleanclassfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)ChipRemove.State
type ComboboxChipRemoveState = {
/** Whether the component should ignore user interaction. */
disabled: boolean;
};List
A list container for the items.
Renders a <div> element.
childrenfunction—
JSX.Element | ((item: any, index: number) => JSX.Element)classfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)List.State
type ComboboxListState = {
/** Whether the list is empty. */
empty: boolean;
};Portal
A portal element that moves the popup to a different part of the DOM.
By default, the portal element is appended to <body>.
Renders a <div> element.
containerUnion—
HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | nullclassfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)keepMountedbooleanfalse
booleanrenderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Portal.State
type ComboboxPortalState = {};Backdrop
An overlay displayed beneath the popup.
Renders a <div> element.
classfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Data attributes
data-open-—
-data-closed-—
-data-starting-style-—
-data-ending-style-—
-Attribute | Description | |
|---|---|---|
data-open | Present when the popup is open. | |
data-closed | Present when the popup is closed. | |
data-starting-style | Present when the popup begins animating in. | |
data-ending-style | Present when the popup is animating out. | |
Backdrop.State
type ComboboxBackdropState = {
/** Whether the popup is currently open. */
open: boolean;
/** The transition status of the component. */
transitionStatus: TransitionStatus;
};Positioner
Positions the popup against the trigger.
Renders a <div> element.
disableAnchorTrackingbooleanfalse
booleanalignAlign'center'
AlignalignOffsetUnion0
data object parameter with the following properties: data.anchor: the dimensions of the anchor element with properties width and height.data.positioner: the dimensions of the positioner element with properties width and height.data.side: which side of the anchor element the positioner is aligned against.data.align: how the positioner is aligned relative to the specified side.number | OffsetFunctionsideSide'bottom'
SidesideOffsetUnion0
data object parameter with the following properties: data.anchor: the dimensions of the anchor element with properties width and height.data.positioner: the dimensions of the positioner element with properties width and height.data.side: which side of the anchor element the positioner is aligned against.data.align: how the positioner is aligned relative to the specified side.number | OffsetFunctionarrowPaddingnumber5
numberanchorfunction—
Element | VirtualElement | RefObject<Element | null> | (() => Element | VirtualElement | null) | nullcollisionAvoidanceCollisionAvoidance—
side controls overflow on the preferred placement axis (top/bottom or left/right): 'flip': keep the requested side when it fits; otherwise try the opposite side
(top and bottom, or left and right).'shift': never change side; keep the requested side and move the popup within
the clipping boundary so it stays visible.'none': do not correct side-axis overflow. align controls overflow on the alignment axis (start/center/end): 'flip': keep side, but swap start and end when the requested alignment overflows.'shift': keep side and requested alignment, then nudge the popup along the
alignment axis to fit.'none': do not correct alignment-axis overflow. fallbackAxisSide controls fallback behavior on the perpendicular axis when the
preferred axis cannot fit: 'start': allow perpendicular fallback and try the logical start side first
(top before bottom, or left before right in LTR).'end': allow perpendicular fallback and try the logical end side first
(bottom before top, or right before left in LTR).'none': do not fallback to the perpendicular axis. When side is 'shift', explicitly setting align only supports 'shift' or 'none'.
If align is omitted, it defaults to 'flip'.CollisionAvoidancecollisionBoundaryBoundary'clipping-ancestors'
BoundarycollisionPaddingPadding5
Paddingstickybooleanfalse
booleanpositionMethodUnion'absolute'
position property to use.'absolute' | 'fixed'classfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Data attributes
data-open-—
-data-closed-—
-data-anchor-hidden-—
-data-alignUnion—
'start' | 'center' | 'end'data-empty-—
-data-sideUnion—
'top' | 'bottom' | 'left' | 'right' | 'inline-end' | 'inline-start'Attribute | Description | |
|---|---|---|
data-open | Present when the popup is open. | |
data-closed | Present when the popup is closed. | |
data-anchor-hidden | Present when the anchor is hidden. | |
data-align | Indicates how the popup is aligned relative to specified side. | |
data-empty | Present when the items list is empty. | |
data-side | Indicates which side the popup is positioned relative to the trigger. | |
CSS variables
--anchor-heightnumber—
number--anchor-widthnumber—
number--available-heightnumber—
number--available-widthnumber—
number--transform-originstring—
stringCSS Variable | Description | |
|---|---|---|
--anchor-height | The anchor’s height. | |
--anchor-width | The anchor’s width. | |
--available-height | The available height between the trigger and the edge of the viewport. | |
--available-width | The available width between the trigger and the edge of the viewport. | |
--transform-origin | The coordinates that this element is anchored to. Used for animations and transitions. | |
Positioner.State
type ComboboxPositionerState = {
/** Whether the popup is currently open. */
open: boolean;
/** The side of the anchor the component is placed on. */
side: Side;
/** The alignment of the component relative to the anchor. */
align: Align;
/** Whether the anchor element is hidden. */
anchorHidden: boolean;
/** Whether there are no items to display. */
empty: boolean;
};Popup
A container for the list.
Renders a <div> element.
initialFocusfunction—
false: Do not move focus.true: Move focus based on the default behavior (first tabbable element or popup).RefObject: Move focus to the ref element.function: Called with the interaction type (mouse, touch, pen, or keyboard).
Return an element to focus, true to use the default behavior, or false/undefined to do nothing.boolean | RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | void | HTMLElement | null)finalFocusfunction—
false: Do not move focus.true: Move focus based on the default behavior (trigger or previously focused element).RefObject: Move focus to the ref element.function: Called with the interaction type (mouse, touch, pen, or keyboard).
Return an element to focus, true to use the default behavior, or false/undefined to do nothing.boolean | RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)classfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Data attributes
data-open-—
-data-closed-—
-data-anchor-hidden-—
-data-alignUnion—
'start' | 'center' | 'end'data-empty-—
-data-sideUnion—
'top' | 'bottom' | 'left' | 'right' | 'inline-end' | 'inline-start'data-starting-style-—
-data-ending-style-—
-Attribute | Description | |
|---|---|---|
data-open | Present when the popup is open. | |
data-closed | Present when the popup is closed. | |
data-anchor-hidden | Present when the anchor is hidden. | |
data-align | Indicates how the popup is aligned relative to specified side. | |
data-empty | Present when the items list is empty. | |
data-side | Indicates which side the popup is positioned relative to the trigger. | |
data-starting-style | Present when the popup begins animating in. | |
data-ending-style | Present when the popup is animating out. | |
Popup.State
type ComboboxPopupState = {
/** Whether the component is open. */
open: boolean;
/** The side of the anchor the component is placed on. */
side: Side;
/** The alignment of the component relative to the anchor. */
align: Align;
/** Whether the anchor element is hidden. */
anchorHidden: boolean;
/** The transition status of the component. */
transitionStatus: TransitionStatus;
/** Whether there are no items to display. */
empty: boolean;
};Arrow
Displays an element positioned against the anchor.
Renders a <div> element.
classfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Data attributes
data-open-—
-data-closed-—
-data-uncentered-—
-data-alignUnion—
'start' | 'center' | 'end'data-sideUnion—
'top' | 'bottom' | 'left' | 'right' | 'inline-end' | 'inline-start'Attribute | Description | |
|---|---|---|
data-open | Present when the popup is open. | |
data-closed | Present when the popup is closed. | |
data-uncentered | Present when the arrow is uncentered. | |
data-align | Indicates how the popup is aligned relative to specified side. | |
data-side | Indicates which side the popup is positioned relative to the trigger. | |
Arrow.State
type ComboboxArrowState = {
/** Whether the popup is currently open. */
open: boolean;
/** The side of the anchor the component is placed on. */
side: Side;
/** The alignment of the component relative to the anchor. */
align: Align;
/** Whether the arrow cannot be centered on the anchor. */
uncentered: boolean;
};Status
Displays a status message whose content changes are announced politely to screen readers.
Useful for conveying the status of an asynchronously loaded list.
This component’s root element must remain mounted in the DOM to announce
changes consistently across screen readers. Avoid hiding or removing the
component itself with display: none, hidden, aria-hidden, or conditional
rendering. Prefer updating or conditionally rendering its children instead.
Renders a <div> element.
classfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Status.State
type ComboboxStatusState = {};Empty
Renders its children only when the list is empty.
Requires the items prop on the root component.
Announces changes politely to screen readers.
This component’s root element must remain mounted in the DOM to announce
changes consistently across screen readers. Avoid hiding or removing the
component itself with display: none, hidden, aria-hidden, or conditional
rendering. Prefer updating or conditionally rendering its children instead.
Renders a <div> element.
classfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Empty.State
type ComboboxEmptyState = {};Collection
Renders filtered list items.
Doesn’t render its own HTML element.
If rendering a flat list, pass a function child to the List component instead, which implicitly wraps it.
children\*function—
((item: any, index: number) => JSX.Element)Collection.State
type ComboboxCollectionState = {};Row
Displays a single row of items in a grid list.
Enable grid on the root component to turn the listbox into a grid.
Renders a <div> element.
classfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Row.State
type ComboboxRowState = {};Item
An individual item in the list.
Renders a <div> element.
valueanynull
anyonClickfunction—
Enter with the keyboard if the item is highlighted when the Input or List element has focus.((event: BaseUIEvent<MouseEvent>) => void)indexnumber—
numbernativeButtonbooleanfalse
<button> element when replacing it
via the render prop.
Set to true if the rendered element is a native button.booleandisabledbooleanfalse
booleanchildrenJSX.Element—
JSX.Elementclassfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Data attributes
data-selected-—
-data-highlighted-—
-data-disabled-—
-Attribute | Description | |
|---|---|---|
data-selected | Present when the item is selected. | |
data-highlighted | Present when the item is highlighted. | |
data-disabled | Present when the item is disabled. | |
Item.State
type ComboboxItemState = {
/** Whether the item should ignore user interaction. */
disabled: boolean;
/** Whether the item is selected. */
selected: boolean;
/** Whether the item is highlighted. */
highlighted: boolean;
};ItemIndicator
Indicates whether the item is selected.
Renders a <span> element.
childrenJSX.Element—
JSX.Elementclassfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)keepMountedbooleanfalse
booleanrenderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Data attributes
data-starting-style-—
-data-ending-style-—
-Attribute | Description | |
|---|---|---|
data-starting-style | Present when the indicator begins animating in. | |
data-ending-style | Present when the indicator is animating out. | |
ItemIndicator.State
type ComboboxItemIndicatorState = {
/** Whether the item is selected. */
selected: boolean;
/** The transition status of the component. */
transitionStatus: TransitionStatus;
};Group
Groups related items with the corresponding label.
Renders a <div> element.
itemsany[]—
Collection components will use these items.any[]classfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Group.State
type ComboboxGroupState = {};GroupLabel
An accessible label that is automatically associated with its parent group.
Renders a <div> element.
classfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)GroupLabel.State
type ComboboxGroupLabelState = {};Separator
A visual separator between items or groups.
Renders a <div> element.
orientationOrientation'horizontal'
Orientationclassfunction—
JSX.ClassValue | ((state) => JSX.ClassValue)stylefunction—
JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)renderfunction—
keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)Data attributes
data-orientationUnion—
'horizontal' | 'vertical'Attribute | Description | |
|---|---|---|
data-orientation | Indicates the orientation of the separator. | |
Separator.State
type ComboboxSeparatorState = {
/** The orientation of the separator. */
orientation: Orientation;
};useFilter
Matches items against a query using Intl.Collator for robust string matching.
This hook is used when externally filtering items.
Pass the result to the filter prop of <Combobox.Root>.
Matches items against a query using Intl.Collator for robust string matching.
optionsComboboxFilterOptions{}
ComboboxFilterOptionsuseFilter
type ReturnValue = ComboboxFilter;
useFilteredItems
Returns the internally filtered items when called inside <Combobox.Root>.
Returns the internally filtered items. Treat the result as read-only: it is internal state and may be a shared frozen array. Solid returns an accessor; call it in a reactive scope to read the current items.
useFilteredItems
type ReturnValue = Accessor<T[]>;
createItems
Normalizes items into a collection for the items prop of <Combobox.Root>, deriving each item’s selection value and label before rendering.
Creates a collection for the root’s items prop. Values and labels are derived on first use.
Accepts either a flat item array or an array of groups. The getValue and getLabel accessors
receive items, not groups.
Items cannot have an items array property because they would be interpreted as groups.
Rename that field or cast the data when the runtime values are known not to contain arrays.
The data must not contain nullish entries: remove them before creating the collection, as for
the root’s items prop.
Create static collections at module scope. Wrap dynamic collections in createMemo() that reads
their reactive data.
createItems
type ReturnValue = ComboboxItemCollection<Item, ComboboxPrimitiveValue>;