Combobox
Users can either type a value or pick one from a dropdown list.
import { component$, useStyles$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const fruits = [
'Apple',
'Apricot',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Currant',
'Cherry',
'Coconut',
];
return (
<Combobox.Root class="combobox-root">
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{fruits.map((fruit) => (
<Combobox.Item key={fruit} class="combobox-item">
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
✨ Features
- WAI ARIA Combobox design pattern
- Single and multiple selection
- Reactive and initial value changes
- Disabled items
- Tab key focus management
- Grouped items
- Looping
- Custom scroll behavior
- Popover UI above all
- Custom positioning (Popover)
- Typeahead item selection and focus
- Arrow key navigation and focus management
- Open/Close popover by typing, focus, or manually
- Custom filter function
- Closes on no matching items
- Browser autofill with hidden select
- Form validation support
- Custom placeholder
Roadmap
Building blocks
import { component$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuCheck } from '@qwikest/icons/lucide';
export default component$(() => {
return (
<Combobox.Root>
<Combobox.Label>label</Combobox.Label>
<Combobox.Control>
<Combobox.Input />
<Combobox.Trigger>trigger</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover>
<Combobox.Item>
<Combobox.ItemLabel>item label</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
</Combobox.Popover>
</Combobox.Root>
);
});
🎨 Anatomy
Component | Description |
Combobox.Root | Container for the Combobox. |
Combobox.Label | Accessible label for the Combobox. |
Combobox.Control | Central element around which other elements are positioned. |
Combobox.Trigger | Opens and closes the Combobox. |
Combobox.Input | Filters items and shows the selected value. |
Combobox.Popover | Container for list items, positioned above other content. Also the popover. |
Combobox.Item | item in the popover. |
Combobox.ItemLabel | Label for an item. |
Combobox.ItemIndicator | Indicates selection. |
Combobox.Group | Groups related items. |
Combobox.GroupLabel | Label for a group of items. |
Combobox.Description | Displays the accessible description of the combobox. |
Why use a headless Combobox?
A native combobox does not exist. The Open UI group has created a proposal for a new HTML element called combobox
.
Passing data
To add data, use the <Combobox.Item>
component inside of the popover.
Basic example
import { component$, useStyles$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuChevronDown } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
return (
<Combobox.Root class="combobox-root">
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
<Combobox.Item class="combobox-item">
<Combobox.ItemLabel>Option 1</Combobox.ItemLabel>
</Combobox.Item>
<Combobox.Item class="combobox-item">
<Combobox.ItemLabel>Option 2</Combobox.ItemLabel>
</Combobox.Item>
</Combobox.Popover>
</Combobox.Root>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
By default, the content inside of the <Combobox.ItemLabel />
component is the item's value.
Mapping over data
import { component$, useStyles$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const fruits = [
'Apple',
'Apricot',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Currant',
'Cherry',
'Coconut',
];
return (
<Combobox.Root class="combobox-root">
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{fruits.map((fruit) => (
<Combobox.Item key={fruit} class="combobox-item">
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
You control how the data is rendered. Map over the data or render the items as you like.
Object example
import { component$, useStyles$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuChevronDown } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const users = [
{ name: 'Tim', status: '🟢' },
{ name: 'Ryan', status: '🔴' },
{ name: 'Jim', status: '🟡' },
{ name: 'Jessie', status: '🟢' },
{ name: 'Abby', status: '🟡' },
];
return (
<Combobox.Root class="combobox-root">
<Combobox.Label class="combobox-label">Logged in users</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{users.map((user) => (
<Combobox.Item key={user.name} class="combobox-item">
<Combobox.ItemLabel>{user.name}</Combobox.ItemLabel>
<Combobox.ItemIndicator>{user.status}</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
Passing a distinct value
The selected value is: null
import { $, component$, useSignal, useStyles$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
export default component$(() => {
useStyles$(styles);
const users = [
{ id: '0', label: 'Tim' },
{ id: '1', label: 'Ryan' },
{ id: '2', label: 'Jim' },
{ id: '3', label: 'Jessie' },
{ id: '4', label: 'Abby' },
];
const selected = useSignal<string | null>(null);
const handleChange$ = $((value: string) => {
selected.value = value;
});
return (
<>
<Combobox.Root onChange$={handleChange$} class="combobox-root" value="Blackberry">
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{users.map((user) => (
<Combobox.Item value={user.id} key={user.id} class="combobox-item">
<Combobox.ItemLabel>{user.label}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
<p>The selected value is: {selected.value ?? 'null'}</p>
</>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
A distinct value is when one thing is displayed to the user, but another value is passed to the item.
By adding the value
prop to the <Combobox.Item />
component, a distinct value is created.
Handling selection changes
You have changed 0 times
import { component$, useSignal, useStyles$, $ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const count = useSignal(0);
const fruits = [
'Apple',
'Apricot',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Currant',
'Cherry',
'Coconut',
];
const handleChange$ = $(() => {
count.value++;
});
return (
<>
<Combobox.Root class="combobox-root" onChange$={handleChange$}>
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{fruits.map((fruit) => (
<Combobox.Item key={fruit} class="combobox-item">
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
<p>You have changed {count.value} times</p>
</>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
Use the onChange$
prop to listen for changes in the selected value. It provides the new selected value as an argument.
The example above increments a count when a new item is selected.
Component state
To add initial state, use the value
prop on the <Combobox.Root />
component.
Uncontrolled / Initial value
import { component$, useStyles$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const fruits = [
'Apple',
'Apricot',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Currant',
'Cherry',
'Coconut',
];
return (
<Combobox.Root class="combobox-root" value="Blackberry">
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{fruits.map((fruit) => (
<Combobox.Item key={fruit} class="combobox-item">
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
The example above sets "Jessie" as the initial value, so it is selected and focused.
Controlled / Reactive value
To add reactive state, use the bind:value
prop on the <Combobox.Root />
component.
Your favorite fruit is: Apricot
import { component$, useSignal, useStyles$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const selectedFruit = useSignal<string>('Apricot');
const fruits = [
'Apple',
'Apricot',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Currant',
'Cherry',
'Coconut',
];
return (
<>
<Combobox.Root class="combobox-root" bind:value={selectedFruit}>
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{fruits.map((fruit) => (
<Combobox.Item key={fruit} class="combobox-item">
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
<p>Your favorite fruit is: {selectedFruit.value}</p>
</>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
Programmatic changes
Optionally, the selected value can be programmatically updated by the signal.
import { $, component$, useSignal, useStyles$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
export default component$(() => {
useStyles$(styles);
const users = [
{ id: '0', name: 'Tim' },
{ id: '1', name: 'Ryan' }, // 👈 start with Ryan
{ id: '2', name: 'Jim' },
{ id: '3', name: 'Jessie' },
{ id: '4', name: 'Abby' },
];
const selectedId = useSignal<string>('1');
return (
<>
<Combobox.Root class="combobox-root" bind:value={selectedId}>
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{users.map((user) => (
<Combobox.Item value={user.id} key={user.id} class="combobox-item">
<Combobox.ItemLabel>{user.name}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
<button onClick$={$(() => (selectedId.value = '4'))}>Change to Abby</button>
</>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
import { LuChevronDown, LuCheck } from '@qwikest/icons/lucide';
In the example above, clicking the "Change to Abby" button changes the selected value.
Disabled items
import { component$, useStyles$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const fruits = [
'Apple',
'Apricot',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Currant',
'Cherry',
'Coconut',
];
return (
<Combobox.Root class="combobox-root">
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{fruits.map((fruit, index) => (
<Combobox.Item
disabled={
index === 0 || index === 2 || index === fruits.length - 1 ? true : false
}
key={fruit}
class="combobox-item"
>
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
Add the disabled
prop to <Combobox.Item />
to disable items.
Disabled items cannot be selected or focused and are skipped during arrow key navigation.
Dynamically adding items
A common use case is adding items dynamically, like an infinite scrolling list of users.
import { $, component$, useSignal, useStyles$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuChevronDown, LuCheck } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const fruits = useSignal<string[]>([
'Apple',
'Apricot',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Currant',
'Cherry',
'Coconut',
]);
const hasAddedFruits = useSignal<boolean>(false);
return (
<>
<Combobox.Root class="combobox-root">
<Combobox.Label class="combobox-label">Fruits</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{fruits.value.map((fruit) => (
<Combobox.Item key={fruit} class="combobox-item">
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
<button
onClick$={$(() => {
if (!hasAddedFruits.value) {
fruits.value = [...fruits.value, 'Durian', 'Jackfruit', 'Ackee'];
hasAddedFruits.value = true;
}
})}
>
Add Fruits
</button>
</>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
Clicking the Add Users
button creates new users to the list. We could also fetch more data from the server or database.
Item indicators
import { component$, useStyles$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const fruits = [
'Apple',
'Apricot',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Currant',
'Cherry',
'Coconut',
];
return (
<Combobox.Root class="combobox-root">
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{fruits.map((fruit) => (
<Combobox.Item key={fruit} class="combobox-item">
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
Add <Combobox.ItemIndicator />
inside <Combobox.Item />
to indicate the selected item.
Multiple selections
To allow multiple selections, set the multiple
prop to true
.
import { component$, useSignal, useStyles$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuCheck, LuChevronDown, LuX } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const fruits = [
'Apple',
'Apricot',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Currant',
'Cherry',
'Coconut',
];
const displayValues = useSignal<string[]>([]);
const selected = useSignal<string[]>([]);
const inputRef = useSignal<HTMLInputElement>();
return (
<Combobox.Root
class="combobox-root"
multiple
removeOnBackspace
bind:displayValue={displayValues}
bind:value={selected}
>
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control combobox-multiple">
<div class="combobox-pill-container">
{displayValues.value.map((item) => (
<span class="combobox-pill" key={item}>
{item}
<span
onPointerDown$={() => {
selected.value = selected.value?.filter(
(selectedItem) => selectedItem !== item,
);
inputRef.value?.focus();
}}
>
<LuX aria-hidden="true" />
</span>
</span>
))}
{displayValues.value.length > 4 && (
<button
class="combobox-clear combobox-pill"
onClick$={() => {
selected.value = [];
inputRef.value?.focus();
}}
>
clear all
</button>
)}
</div>
<Combobox.Input class="combobox-input" ref={inputRef} />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{fruits.map((fruit) => (
<Combobox.Item key={fruit} class="combobox-item">
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
Use the bind:displayValue
signal to configure multiple display values.
You can combine bind:value
and bind:displayValue
to create a custom widget that displays selected values as a list of pills.
Remove items on backspace
To remove items on backspace, add the removeOnBackspace
prop to the <Combobox.Root />
component.
import { component$, useSignal, useStyles$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuCheck, LuChevronDown, LuX } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const fruits = [
'Apple',
'Apricot',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Currant',
'Cherry',
'Coconut',
];
const displayValues = useSignal<string[]>([]);
const selected = useSignal<string[]>([]);
const inputRef = useSignal<HTMLInputElement>();
return (
<Combobox.Root
class="combobox-root"
multiple
removeOnBackspace
bind:displayValue={displayValues}
bind:value={selected}
>
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control combobox-multiple">
<div class="combobox-pill-container">
{displayValues.value.map((item) => (
<span class="combobox-pill" key={item}>
{item}
<span
onPointerDown$={() => {
selected.value = selected.value?.filter(
(selectedItem) => selectedItem !== item,
);
inputRef.value?.focus();
}}
>
<LuX aria-hidden="true" />
</span>
</span>
))}
{displayValues.value.length > 4 && (
<button
class="combobox-clear combobox-pill"
onClick$={() => {
selected.value = [];
inputRef.value?.focus();
}}
>
clear all
</button>
)}
</div>
<Combobox.Input class="combobox-input" ref={inputRef} />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{fruits.map((fruit) => (
<Combobox.Item key={fruit} class="combobox-item">
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
Placeholder
We can provide a custom placeholder to the <Combobox.Root />
component by adding the placeholder
prop.
import { component$, useStyles$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const fruits = [
'Apple',
'Apricot',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Currant',
'Cherry',
'Coconut',
];
return (
<Combobox.Root class="combobox-root" placeholder="placeholder">
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{fruits.map((fruit) => (
<Combobox.Item key={fruit} class="combobox-item">
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
CSR
Like every Qwik UI component, the Combobox component can be rendered server-side or client-side.
import { component$, useStyles$, useSignal } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const renderCombobox = useSignal(false);
const fruits = [
'Apple',
'Apricot',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Currant',
'Cherry',
'Coconut',
];
return (
<>
<button onClick$={() => (renderCombobox.value = true)}>CSR mode</button>
{renderCombobox.value && (
<Combobox.Root class="combobox-root">
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{fruits.map((fruit) => (
<Combobox.Item key={fruit} class="combobox-item">
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
)}
</>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
Menu behavior
Custom Filter
By default, the Combobox filters items based on user input. To disable this, set the filter
prop to false
.
import { component$, useSignal, useStyles$, useTask$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
import { matchSorter } from 'match-sorter';
export default component$(() => {
useStyles$(styles);
const inputValue = useSignal('');
const filteredItems = useSignal<string[]>([]);
const fruits = [
'Apple',
'Apricot',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Currant',
'Cherry',
'Coconut',
];
useTask$(({ track }) => {
track(() => inputValue.value);
filteredItems.value = matchSorter(fruits, inputValue.value);
});
return (
<Combobox.Root class="combobox-root" filter={false}>
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input bind:value={inputValue} class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{filteredItems.value.map((fruit) => (
<Combobox.Item key={fruit} class="combobox-item">
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
You can use any filtering library. In the example above, we use match-sorter
.
Empty UI
By default, the popover automatically closes when there are no items to display.
To show UI when there are no items in the popover, use the Combobox.Empty
component.
import { component$, useStyles$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const fruits = [
'Apple',
'Apricot',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Currant',
'Cherry',
'Coconut',
];
return (
<Combobox.Root class="combobox-root">
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{fruits.map((fruit) => (
<Combobox.Item key={fruit} class="combobox-item">
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
<Combobox.Empty>No items found</Combobox.Empty>
</Combobox.Popover>
</Combobox.Root>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
Handling popover open/close
To handle the popover open/close state, use the onOpenChange$
prop. It passes a boolean indicating whether the popover is open.
The listbox opened and closed 0 time(s)
import { component$, useSignal, useStyles$, $ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const count = useSignal(0);
const fruits = [
'Apple',
'Apricot',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Currant',
'Cherry',
'Coconut',
];
const handleOpenChange$ = $(() => {
count.value++;
});
return (
<>
<Combobox.Root class="combobox-root" onOpenChange$={handleOpenChange$}>
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{fruits.map((fruit) => (
<Combobox.Item key={fruit} class="combobox-item">
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
<p>The listbox opened and closed {count.value} time(s)</p>
</>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
Reactive open state
To reactively control the open state, use the bind:open
prop.
import { component$, useSignal, useStyles$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const isOpen = useSignal(false);
const fruits = [
'Apple',
'Apricot',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Currant',
'Cherry',
'Coconut',
];
return (
<>
<Combobox.Root class="combobox-root" bind:open={isOpen}>
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{fruits.map((fruit) => (
<Combobox.Item key={fruit} class="combobox-item">
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
<button onClick$={() => (isOpen.value = !isOpen.value)}>Toggle open state</button>
</>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
Handling input changes
Add the onInput$
prop to the <Combobox.Root />
component.
onInput$ was called 0 time(s)
import { component$, useSignal, useStyles$, $ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const count = useSignal(0);
const inputValue = useSignal('');
const fruits = [
'Apple',
'Apricot',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Currant',
'Cherry',
'Coconut',
];
const handleInput$ = $((value: string) => {
count.value++;
inputValue.value = value;
});
return (
<>
<Combobox.Root class="combobox-root" onInput$={handleInput$}>
<div>onInput$ value: {inputValue.value}</div>
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{fruits.map((fruit) => (
<Combobox.Item key={fruit} class="combobox-item">
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
<p>onInput$ was called {count.value} time(s)</p>
</>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
This function is called when the user types in the input.
Reactive input value
The input can also be fully controlled by adding the bind:value
prop to the <Combobox.Input />
component.
Typed input string: aba
import { component$, useSignal, useStyles$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const inputStr = useSignal<string>('aba');
const fruits = [
'Apple',
'Apricot',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Currant',
'Cherry',
'Coconut',
];
return (
<>
<Combobox.Root class="combobox-root">
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input bind:value={inputStr} class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{fruits.map((fruit) => (
<Combobox.Item key={fruit} class="combobox-item">
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
<p>Typed input string: {inputStr.value}</p>
</>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
Looping
To loop through items, use the loop
prop on <Combobox.Root />
.
import { component$, useStyles$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const fruits = [
'Apple',
'Apricot',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Currant',
'Cherry',
'Coconut',
];
return (
<Combobox.Root class="combobox-root" loop>
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{fruits.map((fruit) => (
<Combobox.Item key={fruit} class="combobox-item">
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
- Pressing the down arrow key moves focus to the first enabled item.
- Pressing the up arrow key moves focus to the last enabled item.
Grouped Items
Use <Combobox.Group />
and <Combobox.GroupLabel />
to group items and provide an accessible name.
import { component$, useStyles$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuChevronDown } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const activeUsers = ['Tim', 'Ryan', 'Jim', 'Abby'];
const offlineUsers = ['Joey', 'Bob', 'Jack', 'John'];
return (
<Combobox.Root class="combobox-root">
<Combobox.Label class="combobox-label">User count</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
<Combobox.Group>
<Combobox.GroupLabel class="combobox-group-label">Active</Combobox.GroupLabel>
{activeUsers.map((user) => (
<Combobox.Item key={user}>
<Combobox.ItemLabel>{user}</Combobox.ItemLabel>
</Combobox.Item>
))}
</Combobox.Group>
<Combobox.Group>
<Combobox.GroupLabel class="combobox-group-label">Offline</Combobox.GroupLabel>
{offlineUsers.map((user) => (
<Combobox.Item key={user}>
<Combobox.ItemLabel>{user}</Combobox.ItemLabel>
</Combobox.Item>
))}
</Combobox.Group>
</Combobox.Popover>
</Combobox.Root>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
Scrolling
Handle scrolling in the popover since focus remains on the select trigger when open.
import { component$, useStyles$ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuChevronDown } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const activeUsers = ['Tim', 'Ryan', 'Jim', 'Abby'];
const offlineUsers = ['Joey', 'Bob', 'Jack', 'John'];
return (
<Combobox.Root class="combobox-root">
<Combobox.Label class="combobox-label">User count</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger class="combobox-trigger">
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover combobox-max-height" gutter={8}>
<Combobox.Group>
<Combobox.GroupLabel class="combobox-group-label">Active</Combobox.GroupLabel>
{activeUsers.map((user) => (
<Combobox.Item key={user}>
<Combobox.ItemLabel>{user}</Combobox.ItemLabel>
</Combobox.Item>
))}
</Combobox.Group>
<Combobox.Group>
<Combobox.GroupLabel class="combobox-group-label">Offline</Combobox.GroupLabel>
{offlineUsers.map((user) => (
<Combobox.Item key={user}>
<Combobox.ItemLabel>{user}</Combobox.ItemLabel>
</Combobox.Item>
))}
</Combobox.Group>
</Combobox.Popover>
</Combobox.Root>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
The native scrollIntoView
method scrolls items into view when highlighted. To customize scroll behavior, add the scrollOptions
prop to <Combobox.Root />
.
When a value is not selected, the placeholder is displayed.
Advanced
Using Refs (experimental)
You can pass a ref to each Combobox component to access the underlying DOM element. This is an experimental feature, and we are still working on it, use at your own risk.
Click the trigger to change the background color
import { component$, useSignal, useStyles$, $ } from '@builder.io/qwik';
import { Combobox } from '@qwik-ui/headless';
import { LuCheck, LuChevronDown } from '@qwikest/icons/lucide';
export default component$(() => {
useStyles$(styles);
const triggerRef = useSignal<HTMLButtonElement>();
const handleClick$ = $(() => {
if (!triggerRef.value) return;
triggerRef.value.style.backgroundColor = 'red';
});
const fruits = [
'Apple',
'Apricot',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Currant',
'Cherry',
'Coconut',
];
return (
<>
<Combobox.Root class="combobox-root">
<Combobox.Label class="combobox-label">Personal Trainers</Combobox.Label>
<Combobox.Control class="combobox-control">
<Combobox.Input class="combobox-input" />
<Combobox.Trigger
class="combobox-trigger"
onClick$={handleClick$}
ref={triggerRef}
>
<LuChevronDown class="combobox-icon" />
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Popover class="combobox-popover" gutter={8}>
{fruits.map((fruit) => (
<Combobox.Item key={fruit} class="combobox-item">
<Combobox.ItemLabel>{fruit}</Combobox.ItemLabel>
<Combobox.ItemIndicator>
<LuCheck />
</Combobox.ItemIndicator>
</Combobox.Item>
))}
</Combobox.Popover>
</Combobox.Root>
<p>Click the trigger to change the background color</p>
</>
);
});
// internal
import styles from '../snippets/combobox.css?inline';
Above is an example that programmatically changes the background color of the trigger when clicked.
Forms
To use the combobox in a form, a visually hidden native select element is provided inside of <Combobox.HiddenNativeSelect>
.