Combobox
A customizable text field with a listbox, enabling users to constrain a list of choices based on their search criteria.
- List of
- Alice
- Joana
- Malcolm
- Zack
- Brian
- Ryan
- Joe
- Randy
- David
- Joseph
import {
Combobox,
ComboboxControl,
ComboboxIcon,
ComboboxInput,
ComboboxLabel,
ComboboxListbox,
ComboboxOption,
ComboboxPopover,
ComboboxTrigger,
ResolvedOption,
} from '@qwik-ui/headless';
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const selectedOptionIndexSig = useSignal<number>(-1);
const objectExample = [
{ testValue: 'alice', testLabel: 'Alice', disabled: true },
{ testValue: 'joana', testLabel: 'Joana', disabled: true },
{ testValue: 'malcolm', testLabel: 'Malcolm', disabled: false },
{ testValue: 'zack', testLabel: 'Zack', disabled: true },
{ testValue: 'brian', testLabel: 'Brian', disabled: false },
{ testValue: 'ryan', testLabel: 'Ryan', disabled: false },
{ testValue: 'joe', testLabel: 'Joe', disabled: false },
{ testValue: 'randy', testLabel: 'Randy', disabled: false },
{ testValue: 'david', testLabel: 'David', disabled: true },
{ testValue: 'joseph', testLabel: 'Joseph', disabled: false },
];
type MyData = {
testValue: string;
testLabel: string;
disabled: boolean;
};
return (
<Combobox
options={objectExample}
optionValueKey="testValue"
optionLabelKey="testLabel"
optionDisabledKey="disabled"
bind:selectedIndex={selectedOptionIndexSig}
>
<ComboboxLabel class="font-semibold">Personal Trainers ⚡</ComboboxLabel>
<ComboboxControl class="relative flex items-center rounded-base border">
<ComboboxInput
placeholder="Jim"
class="px-d2 w-44 rounded-base bg-background px-2 pr-6 placeholder:text-muted-foreground"
/>
<ComboboxTrigger class="group absolute right-0 h-6 w-6">
<ComboboxIcon class="stroke-white transition-transform duration-500 group-aria-expanded:-rotate-180" />
</ComboboxTrigger>
</ComboboxControl>
<ComboboxPopover class="rounded-sm" gutter={8}>
<ComboboxListbox
class="w-44 rounded-base border-[1px] border-slate-400 bg-slate-900 px-4 py-2"
optionRenderer$={(option: ResolvedOption, index: number) => {
const myData = option.option as MyData;
return (
<ComboboxOption
key={option.key}
resolved={option}
index={index}
class="group flex justify-between rounded-base border border-transparent px-2 hover:bg-accent aria-disabled:font-light aria-disabled:text-muted-foreground aria-disabled:hover:bg-muted aria-selected:cursor-pointer aria-selected:border-border aria-selected:bg-accent"
>
<span>{myData.testLabel}</span>
{selectedOptionIndexSig.value === index && <span>Selected</span>}
</ComboboxOption>
);
}}
/>
</ComboboxPopover>
</Combobox>
);
});
Qwik UI's Combobox implementation follows the WAI-Aria Combobox specifications, along with some additional API's that enhance the flexibility, types, and performance.
✨ Features
- Full WAI-Aria compliance
- Full keyboard navigation
- Custom Autocomplete behavior
- Custom filter functionality
- Controlled or uncontrolled
- Supports disabled options
- Custom signal binds
- Animatable, dynamic, and resumable
Building blocks
import { component$ } from '@builder.io/qwik';
import {
Combobox,
ComboboxLabel,
ComboboxControl,
ComboboxInput,
ComboboxTrigger,
ComboboxPopover,
ComboboxListbox,
ComboboxOption,
ResolvedOption,
} from '@qwik-ui/headless';
export default component$(() => {
const data = ['a', 'b', 'c'];
return (
<Combobox options={data}>
<ComboboxLabel>Label Element</ComboboxLabel>
<ComboboxControl>
<ComboboxInput />
<ComboboxTrigger>Opens Listbox</ComboboxTrigger>
</ComboboxControl>
<ComboboxPopover>
<ComboboxListbox
optionRenderer$={(option: ResolvedOption, index: number) => (
<ComboboxOption index={index} resolved={option}>
Option Label
</ComboboxOption>
)}
/>
</ComboboxPopover>
</Combobox>
);
});
🎨 Anatomy
Component | Description |
Combobox | The root container for the Combobox. |
ComboboxLabel | A label element to connect to the Combobox. |
ComboboxControl | A container for the combobox trigger, and input. |
ComboboxInput | An input element used for filtering, and controlling the combobox state. |
ComboboxTrigger | A button that toggles the corresponding listbox when clicked. |
ComboboxPopover | A Popover component that teleports the children outside its parent when the listbox state is open. |
ComboboxListbox | An unordered list that contains multiple options that display when opened. |
ComboboxOption | A list item that is shown based on the listbox state and filter API. |
Passing data
Qwik UI's Combobox supports both string and option data. Whether that's an array of strings, or an array of objects. To pass in data, add the options
prop on the Combobox Root.
String example
- List of
- Apple
- Apricot
- Avocado 🥑
- Banana
- Bilberry
- Blackberry
- Blackcurrant
- Blueberry
- Boysenberry
- Currant
- Cherry
- Coconut
- Cranberry
- Cucumber
import {
Combobox,
ComboboxControl,
ComboboxIcon,
ComboboxInput,
ComboboxLabel,
ComboboxListbox,
ComboboxOption,
ComboboxPopover,
ComboboxTrigger,
ResolvedOption,
} from '@qwik-ui/headless';
import { component$ } from '@builder.io/qwik';
export default component$(() => {
const fruits = [
'Apple',
'Apricot',
'Avocado 🥑',
'Banana',
'Bilberry',
'Blackberry',
'Blackcurrant',
'Blueberry',
'Boysenberry',
'Currant',
'Cherry',
'Coconut',
'Cranberry',
'Cucumber',
];
return (
<Combobox
class="w-fit"
options={fruits}
filter$={(value: string, options) =>
options.filter(({ option }) => {
return option.toLowerCase().startsWith(value.toLowerCase());
})
}
>
<ComboboxLabel class=" font-semibold">Fruits 🍓</ComboboxLabel>
<ComboboxControl class="relative flex items-center rounded-base border">
<ComboboxInput
class="px-d2 w-44 rounded-base bg-background px-2 pr-6 placeholder:text-muted-foreground"
placeholder="Papaya"
/>
<ComboboxTrigger class="group absolute right-0 h-6 w-6">
<ComboboxIcon class="stroke-foreground transition-transform duration-500 group-aria-expanded:-rotate-180" />
</ComboboxTrigger>
</ComboboxControl>
<ComboboxPopover gutter={8}>
<ComboboxListbox
class="w-44 rounded-base border-[1px] border-slate-400 bg-slate-900 px-4 py-2"
optionRenderer$={(option: ResolvedOption, index: number) => (
<ComboboxOption
key={option.key}
class="group flex justify-between rounded-base border border-transparent px-2 hover:bg-accent aria-disabled:font-light aria-disabled:text-muted-foreground aria-disabled:hover:bg-muted aria-selected:cursor-pointer aria-selected:border-border aria-selected:bg-accent"
index={index}
resolved={option}
>
{option.label}
</ComboboxOption>
)}
/>
</ComboboxPopover>
</Combobox>
);
});
Under the hood, the data passed in populates the listbox with options. To see options visually, add the renderOption$
prop on the Combobox Listbox. This prop is a QRL, inside is a callback where the ComboboxOption
component needs to be passed in.
Object example
- List of
- Anakin Skywalker
- Obi-Wan Kenobi
- Mace Windu
- Yoda
import {
Combobox,
ComboboxControl,
ComboboxIcon,
ComboboxInput,
ComboboxLabel,
ComboboxListbox,
ComboboxOption,
ComboboxPopover,
ComboboxTrigger,
ResolvedOption,
} from '@qwik-ui/headless';
import { component$ } from '@builder.io/qwik';
export default component$(() => {
type Jedi = {
value: string;
label: string;
};
const objectExample: Array<Jedi> = [
{ value: 'anakin', label: 'Anakin Skywalker' },
{ value: 'obi-wan', label: 'Obi-Wan Kenobi' },
{ value: 'mace', label: 'Mace Windu' },
{ value: 'yoda', label: 'Yoda' },
];
return (
<Combobox class="w-fit" options={objectExample}>
<ComboboxLabel class=" font-semibold">Jedi ⚔️</ComboboxLabel>
<ComboboxControl class="relative flex items-center rounded-base border">
<ComboboxInput class="px-d2 w-44 rounded-base bg-background px-2 pr-6 placeholder:text-muted-foreground" />
<ComboboxTrigger class="group absolute right-0 h-6 w-6">
<ComboboxIcon class="stroke-foreground transition-transform duration-500 group-aria-expanded:-rotate-180" />
</ComboboxTrigger>
</ComboboxControl>
<ComboboxPopover gutter={8}>
<ComboboxListbox
class="w-44 rounded-base border-[1px] border-slate-400 bg-slate-900 px-4 py-2"
optionRenderer$={(option: ResolvedOption, index: number) => (
<ComboboxOption
key={option.key}
class="group flex justify-between rounded-base border border-transparent px-2 hover:bg-accent aria-disabled:font-light aria-disabled:text-muted-foreground aria-disabled:hover:bg-muted aria-selected:cursor-pointer aria-selected:border-border aria-selected:bg-accent"
index={index}
resolved={option}
>
{option.label}
</ComboboxOption>
)}
/>
</ComboboxPopover>
</Combobox>
);
});
The callback takes two parameters:
- An object that holds the resolved option data.
- The option index.
The resolved object also holds:
- The key.
- Option Label.
- Option Value.
- Any disabled options.
Import the ResolvedOption type from Qwik UI and pass in the data according to the code block below.
import { component$ } from '@builder.io/qwik';
import { ComboboxOption, ComboboxListbox, type ResolvedOption } from '@qwik-ui/headless';
export default component$(() => {
return (
<ComboboxListbox
optionRenderer$={(option: ResolvedOption, index: number) => (
<ComboboxOption key={option.key} index={index} resolved={option}>
{option.label}
</ComboboxOption>
)}
/>
);
});
Adding a filter
Out of the box, the Combobox comes with a default filter that uses the string.includes method to filter options. To add a custom filter, you can use the filter$
QRL.
- List of
- United States
- Canada
- Mexico
- Brazil
- United Kingdom
- Germany
- France
- Italy
import {
Combobox,
ComboboxControl,
ComboboxIcon,
ComboboxInput,
ComboboxLabel,
ComboboxListbox,
ComboboxOption,
ComboboxPopover,
ComboboxTrigger,
ResolvedOption,
} from '@qwik-ui/headless';
import { component$ } from '@builder.io/qwik';
export default component$(() => {
type Countries = {
value: string;
label: string;
};
const objectExample: Array<Countries> = [
{ value: 'usa', label: 'United States' },
{ value: 'canada', label: 'Canada' },
{ value: 'mexico', label: 'Mexico' },
{ value: 'brazil', label: 'Brazil' },
{ value: 'uk', label: 'United Kingdom' },
{ value: 'germany', label: 'Germany' },
{ value: 'france', label: 'France' },
{ value: 'italy', label: 'Italy' },
];
return (
<Combobox
class="w-fit"
options={objectExample}
filter$={(value: string, options) =>
options.filter(({ option }) => {
return option.label.toLowerCase().startsWith(value.toLowerCase());
})
}
>
<ComboboxLabel class="font-semibold">Countries 🚩</ComboboxLabel>
<ComboboxControl class="relative flex items-center rounded-base border">
<ComboboxInput class="px-d2 w-44 rounded-base bg-background px-2 pr-6 placeholder:text-muted-foreground" />
<ComboboxTrigger class="group absolute right-0 h-6 w-6">
<ComboboxIcon class="stroke-foreground transition-transform duration-500 group-aria-expanded:-rotate-180" />
</ComboboxTrigger>
</ComboboxControl>
<ComboboxPopover flip={true} gutter={8}>
<ComboboxListbox
class="w-44 rounded-base border-[1px] border-slate-400 bg-slate-900 px-4 py-2"
optionRenderer$={(option: ResolvedOption, index: number) => (
<ComboboxOption
key={option.key}
class="group flex justify-between rounded-base border border-transparent px-2 hover:bg-accent aria-disabled:font-light aria-disabled:text-muted-foreground aria-disabled:hover:bg-muted aria-selected:cursor-pointer aria-selected:border-border aria-selected:bg-accent"
index={index}
resolved={option}
>
{option.label}
</ComboboxOption>
)}
/>
</ComboboxPopover>
</Combobox>
);
});
In this example, the filter function retrieves the input value and options object. It then filters the options using the JavaScript string.startsWith
method as part of our filter logic. However, you are not limited to this method. You can use any filtering method of your choice to customize the behavior to suit your needs.
Sorting
In our previous example, you may have noticed that the country data wasn't sorted alphabetically. To sort options, add an additional sort method after the filter.
- List of
- Brazil
- Canada
- France
- Germany
- Italy
- Mexico
- United Kingdom
- United States
import {
Combobox,
ComboboxControl,
ComboboxIcon,
ComboboxInput,
ComboboxLabel,
ComboboxListbox,
ComboboxOption,
ComboboxPopover,
ComboboxTrigger,
ResolvedOption,
} from '@qwik-ui/headless';
import { component$ } from '@builder.io/qwik';
export default component$(() => {
type Countries = {
value: string;
label: string;
};
const objectExample: Array<Countries> = [
{ value: 'usa', label: 'United States' },
{ value: 'canada', label: 'Canada' },
{ value: 'mexico', label: 'Mexico' },
{ value: 'brazil', label: 'Brazil' },
{ value: 'uk', label: 'United Kingdom' },
{ value: 'germany', label: 'Germany' },
{ value: 'france', label: 'France' },
{ value: 'italy', label: 'Italy' },
];
return (
<Combobox
class="w-fit"
options={objectExample}
filter$={(value: string, options) =>
options
.filter(({ option }) => {
return option.label.toLowerCase().startsWith(value.toLowerCase());
})
.sort((country1, country2) =>
country1.option.label.localeCompare(country2.option.label),
)
}
>
<ComboboxLabel class=" font-semibold">Countries 🚩</ComboboxLabel>
<ComboboxControl class="relative flex items-center rounded-base border">
<ComboboxInput class="px-d2 w-44 rounded-base bg-background px-2 pr-6 placeholder:text-muted-foreground" />
<ComboboxTrigger class="group absolute right-0 h-6 w-6">
<ComboboxIcon class="stroke-foreground transition-transform duration-500 group-aria-expanded:-rotate-180" />
</ComboboxTrigger>
</ComboboxControl>
<ComboboxPopover flip={true} gutter={8}>
<ComboboxListbox
class="w-44 rounded-base border-[1px] border-slate-400 bg-slate-900 px-4 py-2"
optionRenderer$={(option: ResolvedOption, index: number) => (
<ComboboxOption
key={option.key}
class="group flex justify-between rounded-base border border-transparent px-2 hover:bg-accent aria-disabled:font-light aria-disabled:text-muted-foreground aria-disabled:hover:bg-muted aria-selected:cursor-pointer aria-selected:border-border aria-selected:bg-accent"
index={index}
resolved={option}
>
{option.label}
</ComboboxOption>
)}
/>
</ComboboxPopover>
</Combobox>
);
});
Disabled Behavior
There are two ways to indicate that an option is disabled:
- Using the default disabled key.
- Passing a unique key name using the optionDisabledKey prop.
- List of
- Enabled
- Enabled
- Disabled
- Enabled
- Disabled
- Disabled
- Disabled
- Enabled
import {
Combobox,
ComboboxControl,
ComboboxIcon,
ComboboxInput,
ComboboxLabel,
ComboboxListbox,
ComboboxOption,
ComboboxPopover,
ComboboxTrigger,
ResolvedOption,
} from '@qwik-ui/headless';
import { component$ } from '@builder.io/qwik';
export default component$(() => {
type DisabledExample = {
value: string;
label: string;
myDisabledKey: boolean;
};
const disabledExample: Array<DisabledExample> = [
{ value: '0', label: 'Enabled', myDisabledKey: false },
{ value: '1', label: 'Enabled', myDisabledKey: false },
{ value: '2', label: 'Disabled', myDisabledKey: true },
{ value: '3', label: 'Enabled', myDisabledKey: false },
{ value: '4', label: 'Disabled', myDisabledKey: true },
{ value: '5', label: 'Disabled', myDisabledKey: true },
{ value: '6', label: 'Disabled', myDisabledKey: true },
{ value: '7', label: 'Enabled', myDisabledKey: false },
];
return (
<Combobox class="w-fit" options={disabledExample} optionDisabledKey="myDisabledKey">
<ComboboxLabel class=" font-semibold">Disabled ⛔</ComboboxLabel>
<ComboboxControl class="relative flex items-center rounded-base border">
<ComboboxInput class="px-d2 w-44 rounded-base bg-background px-2 pr-6 placeholder:text-muted-foreground" />
<ComboboxTrigger class="group absolute right-0 h-6 w-6">
<ComboboxIcon class="stroke-foreground transition-transform duration-500 group-aria-expanded:-rotate-180" />
</ComboboxTrigger>
</ComboboxControl>
<ComboboxPopover flip={true} gutter={8}>
<ComboboxListbox
class="w-44 rounded-base border-[1px] border-slate-400 bg-slate-900 px-4 py-2"
optionRenderer$={(option: ResolvedOption, index: number) => (
<ComboboxOption
key={option.key}
class="group flex justify-between rounded-base border border-transparent px-2 hover:bg-accent aria-disabled:font-light aria-disabled:text-muted-foreground aria-disabled:hover:bg-muted aria-selected:cursor-pointer aria-selected:border-border aria-selected:bg-accent"
index={index}
resolved={option}
>
{option.label}
</ComboboxOption>
)}
/>
</ComboboxPopover>
</Combobox>
);
});
The optionDisabledKey prop does not need to be passed when there is a key already named disabled, OR you do not have any disabled options.
Custom Key Names
As we saw previously with the optionDisabledKey prop, custom key names can also be passed in for values and labels.
- List of
- Bulbasaur1
- Ivysaur2
- Venusaur3
- Charmander4
- Charmeleon5
- Charizard6
- Squirtle7
- Wartortle8
import {
Combobox,
ComboboxControl,
ComboboxIcon,
ComboboxInput,
ComboboxLabel,
ComboboxListbox,
ComboboxOption,
ComboboxPopover,
ComboboxTrigger,
ResolvedOption,
} from '@qwik-ui/headless';
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
type Pokemon = {
pokedex: string;
pokemon: string;
isPokemonCaught: boolean;
};
const pokemonExample: Array<Pokemon> = [
{ pokedex: '1', pokemon: 'Bulbasaur', isPokemonCaught: true },
{ pokedex: '2', pokemon: 'Ivysaur', isPokemonCaught: false },
{ pokedex: '3', pokemon: 'Venusaur', isPokemonCaught: false },
{ pokedex: '4', pokemon: 'Charmander', isPokemonCaught: true },
{ pokedex: '5', pokemon: 'Charmeleon', isPokemonCaught: true },
{ pokedex: '6', pokemon: 'Charizard', isPokemonCaught: true },
{ pokedex: '7', pokemon: 'Squirtle', isPokemonCaught: false },
{ pokedex: '8', pokemon: 'Wartortle', isPokemonCaught: false },
];
const isPokemonCaught = useSignal(false);
return (
<div>
{isPokemonCaught.value && (
<p class="absolute max-w-[180px] translate-y-[-105%] rounded-base border-2 bg-background p-4 shadow-md">
You've already caught this pokemon!
</p>
)}
<Combobox
class="w-fit"
options={pokemonExample}
optionValueKey="pokedex"
optionLabelKey="pokemon"
optionDisabledKey="isPokemonCaught"
>
<ComboboxLabel class=" font-semibold">Pokemon 🦏</ComboboxLabel>
<ComboboxControl class="relative flex items-center rounded-base border">
<ComboboxInput class="px-d2 w-44 rounded-base bg-background px-2 pr-6 placeholder:text-muted-foreground" />
<ComboboxTrigger class="group absolute right-0 h-6 w-6">
<ComboboxIcon class="stroke-foreground transition-transform duration-500 group-aria-expanded:-rotate-180" />
</ComboboxTrigger>
</ComboboxControl>
<ComboboxPopover flip={true} gutter={8}>
<ComboboxListbox
class="w-44 rounded-base border-[1px] border-slate-400 bg-slate-900 px-4 py-2"
optionRenderer$={(option: ResolvedOption, index: number) => {
const pokemonOption = option.option as Pokemon;
return (
<ComboboxOption
key={option.key}
class="group flex justify-between rounded-base border border-transparent px-2 hover:bg-accent aria-disabled:font-light aria-disabled:text-muted-foreground aria-disabled:hover:bg-muted aria-selected:cursor-pointer aria-selected:border-border aria-selected:bg-accent"
index={index}
resolved={option}
onMouseEnter$={() => {
if (pokemonOption.isPokemonCaught) {
isPokemonCaught.value = true;
}
}}
onMouseLeave$={() => {
isPokemonCaught.value = false;
}}
>
<span>{pokemonOption.pokemon}</span>
<span>{pokemonOption.pokedex}</span>
</ComboboxOption>
);
}}
/>
</ComboboxPopover>
</Combobox>
</div>
);
});
In some cases, your data object keys may not match the default keys that the Combobox component expects for option values and labels. By default, the Combobox component looks for keys named value for option values and label for option labels.
If your data object keys are different, you can specify custom key names using the optionValueKey
and optionLabelKey
props.
import { Combobox } from '@qwik-ui/headless';
import { component$ } from '@builder.io/qwik';
type Pokemon = {
pokedex: string;
pokemon: string;
isPokemonCaught: boolean;
};
const pokemonExample: Array<Pokemon> = [
{ pokedex: '1', pokemon: 'Bulbasaur', isPokemonCaught: true },
{ pokedex: '2', pokemon: 'Ivysaur', isPokemonCaught: false },
{ pokedex: '3', pokemon: 'Venusaur', isPokemonCaught: false },
{ pokedex: '4', pokemon: 'Charmander', isPokemonCaught: true },
{ pokedex: '5', pokemon: 'Charmeleon', isPokemonCaught: true },
{ pokedex: '6', pokemon: 'Charizard', isPokemonCaught: true },
{ pokedex: '7', pokemon: 'Squirtle', isPokemonCaught: false },
{ pokedex: '8', pokemon: 'Wartortle', isPokemonCaught: false },
];
export default component$(() => {
return (
<Combobox
options={pokemonExample}
optionValueKey="pokedex"
optionLabelKey="pokemon"
optionDisabledKey="isPokemonCaught"
/>
);
});
Within our example, the value key is called pokedex and the label key is called pokemon. This tells the Combobox component to use the "pokedex" key for option values and the "pokemon" key for option labels.
Configuring the Input
Disabling Blur
The Qwik UI Combobox uses the onBlur$ event
on the input to close the listbox when interacting with other elements.
I have blur disabled! Inspect me in the dev tools.
- List ofoptions
- Mercury
- Venus
- Earth
- Mars
- Jupiter
- Saturn
- Uranus
- Neptune
import {
Combobox,
ComboboxControl,
ComboboxIcon,
ComboboxInput,
ComboboxListbox,
ComboboxOption,
ComboboxPopover,
ComboboxTrigger,
ResolvedOption,
} from '@qwik-ui/headless';
import { component$ } from '@builder.io/qwik';
export default component$(() => {
const planets = [
'Mercury',
'Venus',
'Earth',
'Mars',
'Jupiter',
'Saturn',
'Uranus',
'Neptune',
];
return (
<div class="flex flex-col items-center">
<p class="text-center">I have blur disabled! Inspect me in the dev tools.</p>
<Combobox
class="w-fit"
options={planets}
filter$={(value: string, options) =>
options.filter(({ option }) => {
return option.toLowerCase().startsWith(value.toLowerCase());
})
}
>
<ComboboxControl class="relative flex items-center rounded-base border">
<ComboboxInput
disableOnBlur={true}
class="px-d2 w-44 rounded-base bg-background px-2 pr-6 placeholder:text-muted-foreground"
/>
<ComboboxTrigger class="group absolute right-0 h-6 w-6">
<ComboboxIcon class="stroke-foreground transition-transform duration-500 group-aria-expanded:-rotate-180" />
</ComboboxTrigger>
</ComboboxControl>
<ComboboxPopover gutter={8} hide="referenceHidden">
<ComboboxListbox
class="w-44 rounded-base border-[1px] border-slate-400 bg-slate-900 px-4 py-2"
optionRenderer$={(option: ResolvedOption, index: number) => (
<ComboboxOption
key={option.key}
class="group flex justify-between rounded-base border border-transparent px-2 hover:bg-accent aria-disabled:font-light aria-disabled:text-muted-foreground aria-disabled:hover:bg-muted aria-selected:cursor-pointer aria-selected:border-border aria-selected:bg-accent"
index={index}
resolved={option}
>
{option.label}
</ComboboxOption>
)}
/>
</ComboboxPopover>
</Combobox>
</div>
);
});
This can get tedious if certain custom behavior needs to be added, or the listbox needs to be inspected in the dev tools.
To disable this behavior, set disableOnBlur={true}
on the Input.
Customizing State Signals
The Combobox component allows you to customize its state signals. This can be useful if you want to control or observe these states externally.
In reality it is a signal prop, we prefer using this syntax, because we believe it is familiar to Qwik developers with the bind syntax.
Prop | Type | Description |
---|---|---|
bind:inputValue | Signal | Controls the current value of the input. |
bind:selectedIndex | Signal | Controls the selected option index. |
bind:isListboxOpen | Signal | Controls the open state of the listbox. |
bind:isInputFocused | Signal | Controls the focus state of the input. |
bind:highlightedIndex | Signal | Controls which option is highlighted. |
Here is an example of how to customize these signals:
I love signals! 🗼
- List ofoptions
- bind:isListboxOpenSig
- bind:isInputFocusedSig
- bind:isTriggerFocusedSig
- bind:inputValueSig
import {
Combobox,
ComboboxControl,
ComboboxIcon,
ComboboxInput,
ComboboxListbox,
ComboboxOption,
ComboboxPopover,
ComboboxTrigger,
ResolvedOption,
} from '@qwik-ui/headless';
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const isListboxOpenSig = useSignal(false);
const highlightedIndexSig = useSignal(2);
const signalsExample = [
'bind:isListboxOpenSig',
'bind:isInputFocusedSig',
'bind:isTriggerFocusedSig',
'bind:inputValueSig',
];
return (
<>
<p class="text-center">I love signals! 🗼</p>
<div>
<Combobox
class="w-fit"
options={signalsExample}
bind:isListboxOpen={isListboxOpenSig}
bind:highlightedIndex={highlightedIndexSig}
>
<ComboboxControl class="relative flex items-center rounded-base border">
<ComboboxInput
class="px-d2 w-fit rounded-base px-2 pr-6 placeholder:text-muted-foreground"
onClick$={() => (isListboxOpenSig.value = !isListboxOpenSig.value)}
/>
<ComboboxTrigger class="group absolute right-0 h-6 w-6">
<ComboboxIcon class="stroke-foreground transition-transform duration-500 group-aria-expanded:-rotate-180" />
</ComboboxTrigger>
</ComboboxControl>
<ComboboxPopover hide="escaped" gutter={8}>
<ComboboxListbox
class="w-fit rounded-base border-[1px] border-slate-400 bg-slate-900 px-4 py-2"
optionRenderer$={(option: ResolvedOption, index: number) => (
<ComboboxOption
key={option.key}
class="group flex justify-between rounded-base border border-transparent px-2 hover:bg-accent aria-disabled:font-light aria-disabled:text-muted-foreground aria-disabled:hover:bg-muted aria-selected:cursor-pointer aria-selected:border-border aria-selected:bg-accent"
index={index}
resolved={option}
>
{option.label}
</ComboboxOption>
)}
/>
</ComboboxPopover>
</Combobox>
</div>
</>
);
});
When clicking on the input, it will now toggle the listbox. Signal binds are a useful toolbelt when customizing state is needed.
Default Label
To set a default Label, pass the defaultLabel
prop to the Combobox Root, along with the string label. This label connects to its proper option value, and highlights it accordingly.
- List of
- John
import {
Combobox,
ComboboxControl,
ComboboxIcon,
ComboboxInput,
ComboboxLabel,
ComboboxListbox,
ComboboxOption,
ComboboxPopover,
ComboboxTrigger,
ResolvedOption,
} from '@qwik-ui/headless';
import { component$ } from '@builder.io/qwik';
export default component$(() => {
const names = ['Jim', 'Joanna', 'John', 'Jessica'];
return (
<Combobox class="w-fit" defaultLabel={names[2]} options={names}>
<ComboboxLabel>Default Label</ComboboxLabel>
<ComboboxControl class="relative rounded-base border">
<ComboboxInput class="px-d2 w-44 rounded-base bg-background px-2 pr-6 placeholder:text-muted-foreground" />
<ComboboxTrigger class="group absolute right-0 h-6 w-6">
<ComboboxIcon class="stroke-foreground transition-transform duration-500 group-aria-expanded:-rotate-180" />
</ComboboxTrigger>
</ComboboxControl>
<ComboboxPopover gutter={8} hide="escaped">
<ComboboxListbox
class="w-44 rounded-base border-[1px] border-slate-400 bg-slate-900 px-4 py-2"
optionRenderer$={(option: ResolvedOption, index: number) => (
<ComboboxOption
key={option.key}
class="group flex justify-between rounded-base border border-transparent px-2 hover:bg-accent aria-disabled:font-light aria-disabled:text-muted-foreground aria-disabled:hover:bg-muted aria-selected:cursor-pointer aria-selected:border-border aria-selected:bg-accent"
index={index}
resolved={option}
>
{option.label}
</ComboboxOption>
)}
/>
</ComboboxPopover>
</Combobox>
);
});
Setting a default highlighted index
With the trick we learned about signal binds earlier, we can highlight an index by default, or customize the highlighted index based on user interaction.
Third option highlighted! 🚨
- List ofoptions
- not highlighted
- not highlighted
- highlighted by default!
- not highlighted
import {
Combobox,
ComboboxControl,
ComboboxIcon,
ComboboxInput,
ComboboxListbox,
ComboboxOption,
ComboboxPopover,
ComboboxTrigger,
ResolvedOption,
} from '@qwik-ui/headless';
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const highlightedIndexSig = useSignal(2);
const highlightedExample = [
'not highlighted',
'not highlighted',
'highlighted by default!',
'not highlighted',
];
return (
<>
<p class="text-center">Third option highlighted! 🚨</p>
<Combobox
class="w-fit"
options={highlightedExample}
bind:highlightedIndex={highlightedIndexSig}
>
<ComboboxControl class="relative flex items-center rounded-base border">
<ComboboxInput class="px-d2 w-fit rounded-base bg-background px-2 pr-6 placeholder:text-muted-foreground" />
<ComboboxTrigger class="group absolute right-0 h-6 w-6">
<ComboboxIcon class="stroke-foreground transition-transform duration-500 group-aria-expanded:-rotate-180" />
</ComboboxTrigger>
</ComboboxControl>
<ComboboxPopover hide="escaped" gutter={8} size={true}>
<ComboboxListbox
class="w-fit rounded-base border-[1px] border-slate-400 bg-slate-900 px-4 py-2"
optionRenderer$={(option: ResolvedOption, index: number) => (
<ComboboxOption
key={option.key}
class="group cursor-pointer rounded-base px-2 aria-selected:bg-accent"
index={index}
resolved={option}
>
{option.label}
</ComboboxOption>
)}
/>
</ComboboxPopover>
</Combobox>
</>
);
});
Search Bar
A common use case for an Autocomplete is a search bar. Here's an example of that using the Combobox.
import {
Combobox,
ComboboxControl,
ComboboxInput,
ComboboxLabel,
ComboboxListbox,
ComboboxOption,
ComboboxPopover,
ComboboxTrigger,
ResolvedOption,
} from '@qwik-ui/headless';
import { PropsOf, component$, useSignal } from '@builder.io/qwik';
import { statusByComponent } from '~/_state/component-statuses';
import { StatusBadge } from '~/components/component-status-badge/component-status-badge';
export default component$(() => {
const inputValueSig = useSignal('');
const highlightedIndexSig = useSignal(0);
const isListboxOpenSig = useSignal(false);
type MyComponents = {
component: string;
label: string;
};
const docsPrefix = '/docs/headless';
const components = [
{ component: 'accordion', label: 'Accordion' },
{ component: 'combobox', label: 'Combobox' },
{ component: 'popover', label: 'Popover' },
{ component: 'select', label: 'Select' },
{ component: 'separator', label: 'Separator' },
{ component: 'tabs', label: 'Tabs' },
{ component: 'toggle', label: 'Toggle' },
{ component: 'tooltip', label: 'Tooltip' },
];
return (
<Combobox
bind:inputValue={inputValueSig}
bind:highlightedIndex={highlightedIndexSig}
bind:isListboxOpen={isListboxOpenSig}
optionValueKey="component"
class="w-fit"
options={components}
>
<ComboboxLabel>Qwik UI ⚡</ComboboxLabel>
<ComboboxControl class="relative rounded-base border">
<ComboboxInput
onClick$={() => (isListboxOpenSig.value = !isListboxOpenSig.value)}
class="px-d2 w-44 rounded-base bg-background pl-6 pr-6 placeholder:text-muted-foreground"
onKeyDown$={(e: KeyboardEvent) => {
if (e.key === 'Enter') {
const inputElement = e.target as HTMLInputElement;
window.location.href = `${docsPrefix}/${inputElement.value.toLowerCase()}`;
}
}}
/>
<ComboboxTrigger class="group absolute left-[4px] h-6 w-6">
<SearchIcon />
</ComboboxTrigger>
{inputValueSig.value.length > 0 && (
// give separate id if two triggers
<button
id="close-button"
aria-label="clear search"
onMouseDown$={() => {
inputValueSig.value = '';
}}
class="absolute right-0 top-0 flex h-6 w-6 items-center justify-center"
>
<ClearIcon class="h-4 w-4" />
</button>
)}
</ComboboxControl>
<ComboboxPopover gutter={8} hide="escaped">
<ComboboxListbox
class="w-44 rounded-base border-[1px] border-slate-400 bg-slate-900 px-1 py-2"
optionRenderer$={(option: ResolvedOption, index: number) => {
const searchOption = option.option as MyComponents;
return (
<a
href={`${docsPrefix}/${searchOption.component}`}
aria-label={option.label}
>
<ComboboxOption
key={option.key}
class="group flex items-start justify-between gap-4 rounded-base border-2 border-transparent px-2 hover:bg-accent"
index={index}
resolved={option}
>
<span>{searchOption.label}</span>
<span class="scale-[0.9]">
<StatusBadge status={statusByComponent.headless[option.label]} />
</span>
</ComboboxOption>
</a>
);
}}
/>
</ComboboxPopover>
</Combobox>
);
});
export function SearchIcon(props: PropsOf<'svg'>, key: string) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 256 256"
{...props}
key={key}
>
<path
fill="currentColor"
d="m228.24 219.76l-51.38-51.38a86.15 86.15 0 1 0-8.48 8.48l51.38 51.38a6 6 0 0 0 8.48-8.48ZM38 112a74 74 0 1 1 74 74a74.09 74.09 0 0 1-74-74Z"
></path>
</svg>
);
}
export function ClearIcon(props: PropsOf<'svg'>, key: string) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 256 256"
{...props}
key={key}
>
<path
fill="currentColor"
d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24Zm37.66 130.34a8 8 0 0 1-11.32 11.32L128 139.31l-26.34 26.35a8 8 0 0 1-11.32-11.32L116.69 128l-26.35-26.34a8 8 0 0 1 11.32-11.32L128 116.69l26.34-26.35a8 8 0 0 1 11.32 11.32L139.31 128Z"
></path>
</svg>
);
}
This example shows MPA navigation using the Search Bar component. For SPA navigation, use Qwik's useNavigate hook.
Keyboard interactions
Key | Description |
---|---|
Enter | |
Down Arrow | |
Up Arrow | |
Escape | |
Home | |
End | |
Delete |
API
The Combobox component API provides a set of properties that allow you to customize the behavior and appearance of the combobox. Here are the notable properties for this component.
Combobox (Root)
Prop | Type | Description |
---|---|---|
options | O[] | An array of options for the combobox. |
filter$ | QRL | A QRL for a custom filter function. |
optionValueKey | string | The key for the option value. |
optionLabelKey | string | The key for the option label. |
optionDisabledKey | string | The key for the disabled option. |
defaultLabel | string | The default label for the combobox. |
bind:selectedIndex | Signal<number> | A signal for the selected option index. |
bind:isListboxOpen | Signal<boolean> | A signal for the open state of the listbox. |
bind:isInputFocused | Signal<boolean> | A signal for the focus state of the input. |
bind:inputValue | Signal<string> | A signal for the current value of the input. |
bind:highlightedIndex | Signal<number> | A signal for the highlighted option index. |
ComboboxTrigger
Prop | Type | Description |
---|---|---|
onMouseDown$ | function | A QRL that toggles the open state of the listbox on mouse down. |
tabIndex | number | The tab index of the trigger button. Set to 0 by default. |
ComboboxInput
Prop | Type | Description |
---|---|---|
disableOnBlur | boolean | Disables the onBlur event on the input. |
onInput$ | function | A QRL for handling input events. |
onBlur$ | function | A QRL for handling blur events. |
onKeyDown$ | function | A QRL for handling keydown events. |
type | string | The type of the input element. |
value | string | The current value of the input element. |
placeholder | string | The placeholder text for the input element. |
ComboboxPopover
The Combobox Popover is a wrapper of Qwik UI's popover component. You can view the full API table at the bottom of the popover page.
ComboboxListbox
Prop | Type | Description |
---|---|---|
optionRenderer$ | QRL | A QRL for rendering the options. |
placement | union "top" | "bottom" | "left" | "right" | The placement of the listbox. |
gutter | number | The gutter space between the input and the floating element. |
flip | boolean | Allows the listbox to flip its position based on available space. |
size | boolean | Controls the size of the listbox. |
autoPlacement | boolean | Automatically places the listbox based on available space. |
hide | union "escaped" | "referenceHidden" | Allows hiding the listbox when it appears detached from the reference element. |
ancestorScroll | boolean | Controls the scroll behavior of the listbox. |
ancestorResize | boolean | Controls the resize behavior of the listbox. |
elementResize | boolean | Controls the resize behavior of the listbox element. |
layoutShift | boolean | Controls the layout shift of the listbox. |
animationFrame | boolean | Whether to update the position of the listbox on every animation frame. |
ComboboxOption
Prop | Type | Description |
---|---|---|
index | number | The index of the option. |
resolved | ResolvedOption
option: O;
key: number;
value: O extends Record<string, unknown> ? O[ValueKey] : O;
label: string;
disabled: boolean;
lcLabel?: string;
| The resolved option data. |
onMouseEnter$ | function | A QRL for handling mouse enter events. |
onClick$ | function | A QRL for handling click events. |
ComboboxIcon
Prop | Type | Description |
---|---|---|
svg | HTMLOrSVGElement | A custom SVG element wrapped around a span with aria-hidden to be used as the icon. If an SVG is not passed inside, it will use the library default caret icon. |