Combobox
An input with a filterable list.
Anatomy
<script>
import { Combobox } from '@shardsui/svelte/combobox'
</script>
<Combobox.Root>
<Combobox.Label />
<Combobox.InputGroup>
<Combobox.Chips>
<Combobox.Chip>
<Combobox.ChipRemove />
</Combobox.Chip>
</Combobox.Chips>
<Combobox.Input />
<Combobox.Trigger>
<Combobox.Value />
</Combobox.Trigger>
<Combobox.Icon />
<Combobox.Clear />
</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.Group>
<Combobox.GroupLabel />
</Combobox.Group>
<Combobox.Separator />
<Combobox.Collection />
</Combobox.List>
</Combobox.Popup>
</Combobox.Positioner>
</Combobox.Portal>
</Combobox.Root>Usage guidelines
- Combobox is a filterable Select: use it when the value is restricted to a predefined set of items (like Select) and you want to narrow that set by typing.
- Not for free-form text: typing only filters the list — the value is always one of the items. For a search widget that accepts arbitrary text, use Autocomplete.
- Not without an input: if you aren't rendering a text input at all, use Select — it carries the accessibility semantics for a listbox that has no input.
- Provide an accessible name: when
<Combobox.Input>is the form control, label it with a native<label>or<Field.Label>, or anaria-labelwhen no visible label is rendered.<Combobox.Label>labels the trigger, not the input — it belongs to the input-inside-popup pattern. See the forms guide. - Pass
itemsfor built-in filtering: the combobox filters theitemsprop internally as the user types; render matches with<Combobox.Collection>inside<Combobox.List>. For async or custom filtering, pass a dynamicitemsarray or thefilteredItems/filterprops — see Filtering.
TypeScript
<Combobox.Root> infers its item type from the value prop, and each entry in the items array must share that type. <Combobox.Item> is not generic — its value is unknown.
See the TypeScript guide for generic roots, typed wrappers, and bind:ref patterns.
Filtering
Pass your data with the items prop and render matches with <Combobox.Collection>:
<script lang="ts">
import { Combobox } from '@shardsui/svelte/combobox'
const courses = [
{ value: 'typography', label: 'Intro to Typography' },
{ value: 'spanish', label: 'Spanish for Beginners' }
/* ... */
]
let value = $state(null)
</script>
<Combobox.Root items={courses} bind:value>
<Combobox.InputGroup>
<Combobox.Input />
</Combobox.InputGroup>
<Combobox.Portal>
<Combobox.Positioner>
<Combobox.Popup>
<Combobox.List>
<Combobox.Collection>
{#snippet children(item)}
<Combobox.Item value={item}>
{item.label}
</Combobox.Item>
{/snippet}
</Combobox.Collection>
</Combobox.List>
</Combobox.Popup>
</Combobox.Positioner>
</Combobox.Portal>
</Combobox.Root>For async search or custom filtering, update the items array from your fetch handler, or pass pre-filtered data with the filteredItems prop and an optional custom filter function. See createFilter below. Or filter in the parent with $derived and render with {#each}.
Examples
Multiple select
Add the multiple prop to let people pick more than one item. <Combobox.Chips>, <Combobox.Chip>, and <Combobox.ChipRemove> render the selected values as removable chips inside the input group, and Backspace on an empty input drops the most recent one.
To keep a long selection from overflowing, slice the values you render inside <Combobox.Chips> and show the rest as a count:
<script>
const CHIP_LIMIT = 3
const visibleValue = $derived(value.slice(0, CHIP_LIMIT))
const hiddenCount = $derived(value.length - visibleValue.length)
</script>
<Combobox.Chips>
{#each visibleValue as item (item)}
<Combobox.Chip>
{item}
<Combobox.ChipRemove aria-label={`Remove ${item}`} />
</Combobox.Chip>
{/each}
{#if hiddenCount > 0}
<span>
{`+${hiddenCount} more`}
</span>
{/if}
<Combobox.Input />
</Combobox.Chips>Grouped
Wrap related items in a <Combobox.Group> with a <Combobox.GroupLabel> heading. Filtering runs within each group, and a group whose items all filter out disappears on its own.
Model each group as one object: an items array of its entries, plus any extra field (value here) that you read when rendering the label.
<script lang="ts">
type TopicGroup = {
value: string
items: string[]
}
const groups: TopicGroup[] = [
{ value: 'Design', items: ['Typography', 'Color theory', 'Layout'] },
{ value: 'Programming', items: ['JavaScript', 'Python', 'Databases'] }
]
</script>Input inside popup
Render the <Combobox.Input> inside the popup itself. Useful when the trigger is a button-like control and the search field appears only when opened.
Here the trigger is the form control, so label it with <Combobox.Label>. It renders a <div>, so a click lands focus on the trigger without opening the popup:
<Combobox.Root>
<Combobox.Label>Instructor</Combobox.Label>
...
</Combobox.Root>Async search (single)
Fetch items as the user types, so nothing loads upfront. Keep the currently selected item in the items array while new results stream in, otherwise the selection drops out of the list mid-fetch. Use <Combobox.Status> to announce loading and <Combobox.Empty> for the no-results state.
Async search (multiple)
Fetch on input changes while allowing several selections. Merge the already-selected items into the items array so their chips stay valid as new matches stream in, and clear the query after each pick so the next search starts fresh.
Creatable
Surface a "Create …" affordance when the typed value doesn't match any existing item. Selecting it opens a dialog to name and confirm the new item before it's added.
Virtualized
For large datasets, renders only the visible items.
API reference
Root
Groups all parts of the combobox.
Doesn't render its own HTML element, but renders a hidden <input> beside — one per selected value in multiple mode.
Label
An accessible label that is automatically associated with the combobox trigger.
Renders a <div> element.
InputGroup
A wrapper for the input and its associated controls.
Renders a <div> element.
Input
A text input to search for items in the list.
Renders an <input> element.
Read-only and required behavior come from <Combobox.Root>'s readOnly and required props.
Trigger
A button that opens the popup.
Renders a <button> element.
Clear
Clears the value when clicked.
Renders a <button> element.
Icon
An icon that indicates that the trigger button opens the popup.
Renders a <span> element.
Chips
A container for the chips in a multiselectable input.
Renders a <div> element.
Chip
An individual chip representing a selected value.
Renders a <div> element.
ChipRemove
A button to remove a chip.
Renders a <button> element.
Value
The current value of the combobox. Doesn't render its own HTML element.
Backdrop
An overlay displayed beneath the popup.
Renders a <div> element.
Portal
A portal that moves the popup out to <body>, clear of ancestor clipping and stacking.
Renders a <div> element.
Positioner
Positions the popup against the trigger.
Renders a <div> element.
Popup
A container for the list.
Renders a <div> element.
List
A list container for the items.
Renders a <div> element.
Collection
Renders filtered list items.
Doesn't render its own HTML element.
Grouped items need a nested pass: an outer <Combobox.Collection> over the groups, and another one inside each <Combobox.Group> for its items.
The children snippet receives (item, index) for each item to render.
Item
An individual item in the list.
Renders a <div> element.
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.
ItemIndicator
Indicates whether the item is selected.
Renders a <span> element.
Empty
Renders its children only when the list is empty — with or without the items prop.
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.
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.
Arrow
Displays an element positioned against the anchor.
Renders a <div> element.
Group
Groups related items with the corresponding label.
Renders a <div> element.
GroupLabel
An accessible label that is automatically associated with its parent group.
Renders a <div> element.
Separator
A visual divider between groups of items. Rendered as role="presentation", because
role="separator" is not valid inside a listbox.
Renders a <div> element.
createFilter
A locale-aware filter helper. Returns three predicates (contains / startsWith / endsWith) built around Intl.Collator, so case- and accent-insensitive matching follows the user's locale. Each predicate also fits the filter prop's signature, so you can hand one to <Combobox.Root> as the internal filter.
It takes Intl.CollatorOptions plus locale, and optionally multiple and value — pass the current selection as value in single-select mode so the selected item keeps matching its own label.
<script>
import { Combobox } from '@shardsui/svelte/combobox'
const filter = Combobox.createFilter({ sensitivity: 'base' })
const filtered = $derived(items.filter((it) => filter.contains(it.label, query)))
</script>Additional types
HighlightReason
The reason passed to onItemHighlighted, reporting what moved the highlight.
type HighlightReason = 'keyboard' | 'pointer' | 'none'