Installation
Install the dependencies
pnpm add cn github:sveltery/baseCopy the component
Copy the following files into src/lib/components/ui/combobox.
It imports the button and input-group components, so copy their folders too.
<script lang="ts">
import type { ComponentProps } from 'svelte';
import { Combobox as Primitive } from '@sveltery/base/combobox';
import { cn } from 'cn';
import { Button } from '../button/index.js';
import XIcon from '@lucide/svelte/icons/x';
let { children, class: classProp, showRemove = true, ...props }: ComponentProps<typeof Primitive.Chip> & { showRemove?: boolean } = $props();
</script>
<Primitive.Chip data-slot="combobox-chip" {...props} class={cn('bg-muted text-foreground flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm px-1.5 text-xs font-medium whitespace-nowrap has-data-[slot=combobox-chip-remove]:pr-0 has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50', classProp)}>
{@render children?.()}
{#if showRemove}
<Primitive.ChipRemove data-slot="combobox-chip-remove" class="-ml-1 opacity-50 hover:opacity-100">
{#snippet render(hostProps, _state, content)}
<Button variant="ghost" size="icon-xs" {...hostProps}>
{@render content?.()}
</Button>
{/snippet}
<XIcon class="pointer-events-none" />
</Primitive.ChipRemove>
{/if}
</Primitive.Chip><script lang="ts">
import type { ComponentProps } from 'svelte';
import { Combobox as Primitive } from '@sveltery/base/combobox';
import { cn } from 'cn';
let { children, class: classProp, ...props }: ComponentProps<typeof Primitive.Chips> = $props();
</script>
<Primitive.Chips data-slot="combobox-chips" {children} {...props} class={cn('dark:bg-input/30 border-input focus-within:border-ring focus-within:ring-ring/50 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive dark:has-aria-invalid:border-destructive/50 flex min-h-8 flex-wrap items-center gap-1 rounded-lg border bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors focus-within:ring-3 has-aria-invalid:ring-3 has-data-[slot=combobox-chip]:px-1', classProp)} /><script lang="ts">
import type { ComponentProps } from 'svelte';
import { Combobox as Primitive } from '@sveltery/base/combobox';
import { cn } from 'cn';
let { class: classProp, ...props }: ComponentProps<typeof Primitive.Input> = $props();
</script>
<Primitive.Input data-slot="combobox-chip-input" {...props} class={cn('min-w-16 flex-1 outline-none', classProp)} /><script lang="ts">
import type { ComponentProps } from 'svelte';
import { Combobox as Primitive } from '@sveltery/base/combobox';
import { cn } from 'cn';
import XIcon from '@lucide/svelte/icons/x';
import { InputGroupButton } from '../input-group/index.js';
let { class: classProp, ...props }: ComponentProps<typeof Primitive.Clear> = $props();
</script>
<Primitive.Clear data-slot="combobox-clear" {...props} class={cn('', classProp)}>
{#snippet render(hostProps, _state, content)}
<InputGroupButton variant="ghost" size="icon-xs" {...hostProps}>
{@render content?.()}
</InputGroupButton>
{/snippet}
<XIcon class="pointer-events-none" />
</Primitive.Clear><script lang="ts">
import type { ComponentProps } from 'svelte';
import { Combobox as Primitive } from '@sveltery/base/combobox';
import { cn } from 'cn';
let {
children,
class: classProp,
side = 'bottom',
sideOffset = 6,
align = 'start',
alignOffset = 0,
anchor,
...props
}: ComponentProps<typeof Primitive.Popup> & Pick<ComponentProps<typeof Primitive.Positioner>, 'align' | 'alignOffset' | 'anchor' | 'side' | 'sideOffset'> = $props();
</script>
<Primitive.Portal>
<Primitive.Positioner {side} {sideOffset} {align} {alignOffset} {anchor} class="isolate z-50">
<Primitive.Popup
data-slot="combobox-content"
data-chips={!!anchor}
{...props}
class={cn('bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:border-input/30 overflow-hidden rounded-lg shadow-md ring-1 duration-100 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:shadow-none data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) data-[chips=true]:min-w-(--anchor-width)', classProp)}
>
{@render children?.()}
</Primitive.Popup>
</Primitive.Positioner>
</Primitive.Portal><script lang="ts">
import type { ComponentProps } from 'svelte';
import { Combobox as Primitive } from '@sveltery/base/combobox';
import { cn } from 'cn';
let { children, class: classProp, ...props }: ComponentProps<typeof Primitive.Empty> = $props();
</script>
<Primitive.Empty data-slot="combobox-empty" {children} {...props} class={cn('text-muted-foreground hidden w-full justify-center py-2 text-center text-sm group-data-empty/combobox-content:flex', classProp)} /><script lang="ts">
import type { ComponentProps } from 'svelte';
import { Combobox as Primitive } from '@sveltery/base/combobox';
import { cn } from 'cn';
let { children, class: classProp, ...props }: ComponentProps<typeof Primitive.Group> = $props();
</script>
<Primitive.Group data-slot="combobox-group" {children} {...props} class={cn('', classProp)} /><script lang="ts">
import type { ComponentProps, Snippet } from 'svelte';
import { Combobox as Primitive } from '@sveltery/base/combobox';
import { cn } from 'cn';
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '../input-group/index.js';
import ComboboxClear from './ComboboxClear.svelte';
import ComboboxTrigger from './ComboboxTrigger.svelte';
let {
children,
class: classProp,
disabled = false,
showTrigger = true,
showClear = false,
...props
}: Omit<ComponentProps<typeof Primitive.Input>, 'children'> & { children?: Snippet; showTrigger?: boolean; showClear?: boolean } = $props();
</script>
<InputGroup class={cn('w-auto', classProp)}>
<Primitive.Input {disabled} {...props}>
{#snippet render(hostProps)}
<InputGroupInput {...hostProps} />
{/snippet}
</Primitive.Input>
<InputGroupAddon align="inline-end">
{#if showTrigger}
<ComboboxTrigger {disabled} data-slot="input-group-button" class="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent">
{#snippet render(hostProps, _state, content)}
<InputGroupButton size="icon-xs" variant="ghost" {...hostProps}>
{@render content?.()}
</InputGroupButton>
{/snippet}
</ComboboxTrigger>
{/if}
{#if showClear}
<ComboboxClear {disabled} />
{/if}
</InputGroupAddon>
{@render children?.()}
</InputGroup><script lang="ts">
import type { ComponentProps } from 'svelte';
import { Combobox as Primitive } from '@sveltery/base/combobox';
import { cn } from 'cn';
import CheckIcon from '@lucide/svelte/icons/check';
let { children, class: classProp, ...props }: ComponentProps<typeof Primitive.Item> = $props();
</script>
<Primitive.Item
data-slot="combobox-item"
{...props}
class={cn("data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground gap-2 rounded-md py-1 pr-8 pl-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 relative flex w-full cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", classProp)}
>
{@render children?.()}
<Primitive.ItemIndicator class="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
<CheckIcon class="pointer-events-none" />
</Primitive.ItemIndicator>
</Primitive.Item><script lang="ts">
import type { ComponentProps } from 'svelte';
import { Combobox as Primitive } from '@sveltery/base/combobox';
import { cn } from 'cn';
let { children, class: classProp, ...props }: ComponentProps<typeof Primitive.GroupLabel> = $props();
</script>
<Primitive.GroupLabel data-slot="combobox-label" {children} {...props} class={cn('text-muted-foreground px-2 py-1.5 text-xs', classProp)} /><script lang="ts">
import type { ComponentProps } from 'svelte';
import { Combobox as Primitive } from '@sveltery/base/combobox';
import { cn } from 'cn';
let { children, class: classProp, ...props }: ComponentProps<typeof Primitive.List> = $props();
</script>
<Primitive.List data-slot="combobox-list" {children} {...props} class={cn('no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 p-1 data-empty:p-0 overflow-y-auto overscroll-contain', classProp)} /><script lang="ts">
import type { ComponentProps } from 'svelte';
import { Combobox as Primitive } from '@sveltery/base/combobox';
import { cn } from 'cn';
let { class: classProp, ...props }: ComponentProps<typeof Primitive.Separator> = $props();
</script>
<Primitive.Separator data-slot="combobox-separator" {...props} class={cn('bg-border -mx-1 my-1 h-px', classProp)} /><script lang="ts">
import type { ComponentProps } from 'svelte';
import { Combobox as Primitive } from '@sveltery/base/combobox';
import { cn } from 'cn';
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
let { children, class: classProp, ...props }: ComponentProps<typeof Primitive.Trigger> = $props();
</script>
<Primitive.Trigger data-slot="combobox-trigger" {...props} class={cn("[&_svg:not([class*='size-'])]:size-4", classProp)}>
{@render children?.()}
<ChevronDownIcon class="text-muted-foreground size-4 pointer-events-none" />
</Primitive.Trigger><script lang="ts">
import type { ComponentProps } from 'svelte';
import { Combobox as Primitive } from '@sveltery/base/combobox';
let { children, ...props }: ComponentProps<typeof Primitive.Value> = $props();
</script>
<Primitive.Value {children} {...props} />import { Combobox as Primitive } from '@sveltery/base/combobox';
export const Combobox = Primitive.Root as typeof Primitive.Root;
export const ComboboxCollection = Primitive.Collection;
export { default as ComboboxInput } from './ComboboxInput.svelte';
export { default as ComboboxContent } from './ComboboxContent.svelte';
export { default as ComboboxList } from './ComboboxList.svelte';
export { default as ComboboxItem } from './ComboboxItem.svelte';
export { default as ComboboxGroup } from './ComboboxGroup.svelte';
export { default as ComboboxLabel } from './ComboboxLabel.svelte';
export { default as ComboboxEmpty } from './ComboboxEmpty.svelte';
export { default as ComboboxSeparator } from './ComboboxSeparator.svelte';
export { default as ComboboxChips } from './ComboboxChips.svelte';
export { default as ComboboxChip } from './ComboboxChip.svelte';
export { default as ComboboxChipsInput } from './ComboboxChipsInput.svelte';
export { default as ComboboxTrigger } from './ComboboxTrigger.svelte';
export { default as ComboboxValue } from './ComboboxValue.svelte';Update the import paths
Change the imports to match your project setup.
Usage
<script lang="ts">
import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList } from '$lib/components/ui/combobox';
const frameworks = ['Next.js', 'SvelteKit', 'Nuxt.js', 'Remix', 'Astro'];
</script><Combobox items={frameworks}>
<ComboboxInput placeholder="Select a framework" />
<ComboboxContent>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
<ComboboxCollection>
{#snippet children(item)}
<ComboboxItem value={item}>{item}</ComboboxItem>
{/snippet}
</ComboboxCollection>
</ComboboxList>
</ComboboxContent>
</Combobox>Pass items to Combobox so it can filter the list as the user types. ComboboxCollection renders its snippet once for each item that matches, so render the items there instead of with {#each}.
Composition
Use the following compositions to build a Combobox.
Simple
A single-line input and a flat list (see Basic).
Combobox
├── ComboboxInput
└── ComboboxContent
├── ComboboxEmpty
└── ComboboxList
└── ComboboxCollection
├── ComboboxItem
└── ComboboxItemWith chips
Multi-select with multiple, chips, and a chips input (see Multiple).
Combobox
├── ComboboxChips
│ └── ComboboxValue
│ ├── ComboboxChip
│ └── ComboboxChipsInput
└── ComboboxContent
├── ComboboxEmpty
└── ComboboxList
└── ComboboxCollection
├── ComboboxItem
└── ComboboxItemWith groups and collection
Nested items per group using ComboboxCollection inside each ComboboxGroup, with a separator between groups (see Groups).
Combobox
├── ComboboxInput
└── ComboboxContent
├── ComboboxEmpty
└── ComboboxList
└── ComboboxCollection
├── ComboboxGroup
│ ├── ComboboxLabel
│ └── ComboboxCollection
│ ├── ComboboxItem
│ └── ComboboxItem
├── ComboboxSeparator
└── ComboboxGroup
├── ComboboxLabel
└── ComboboxCollection
├── ComboboxItem
└── ComboboxItemBinding the value
Bind value to read or change the selection from outside, inputValue to read the text in the input, and open to control the popup. Pass defaultValue to set the starting value.
<script lang="ts">
let framework = $state<string | null>('SvelteKit');
let query = $state('');
</script>
<Combobox items={frameworks} bind:value={framework} bind:inputValue={query}>
...
</Combobox>Custom Items
Use itemToStringValue when your items are objects. It gives the form value and the hidden input a string. itemToStringLabel does the same for the label shown in the input, and an object with a label field needs neither.
<script lang="ts">
type Framework = { label: string; value: string };
const frameworks: Framework[] = [
{ label: 'Next.js', value: 'next' },
{ label: 'SvelteKit', value: 'sveltekit' },
{ label: 'Nuxt', value: 'nuxt' }
];
</script>
<Combobox items={frameworks} itemToStringValue={(framework: Framework) => framework.label}>
<ComboboxInput placeholder="Select a framework" />
<ComboboxContent>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
<ComboboxCollection>
{#snippet children(framework: Framework)}
<ComboboxItem value={framework}>{framework.label}</ComboboxItem>
{/snippet}
</ComboboxCollection>
</ComboboxList>
</ComboboxContent>
</Combobox>Multiple Selection
Use multiple with chips for multi-select behavior. ComboboxValue passes the selected values to its snippet. Attach to ComboboxChips to get its element, and pass it to ComboboxContent as anchor so the popup lines up with the chips instead of the input.
<script lang="ts">
let anchor = $state<HTMLElement | null>(null);
let value = $state<string[]>([]);
</script>
<Combobox items={frameworks} multiple bind:value>
<ComboboxChips {@attach (node) => { anchor = node; }}>
<ComboboxValue>
{#snippet children(values)}
{@const selected = values as string[]}
{#each selected as item (item)}
<ComboboxChip>{item}</ComboboxChip>
{/each}
<ComboboxChipsInput placeholder="Add framework" />
{/snippet}
</ComboboxValue>
</ComboboxChips>
<ComboboxContent {anchor}>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
<ComboboxCollection>
{#snippet children(item)}
<ComboboxItem value={item}>{item}</ComboboxItem>
{/snippet}
</ComboboxCollection>
</ComboboxList>
</ComboboxContent>
</Combobox>Forms
Give Combobox a name and it submits the selected value with its form through a hidden input. Set required to block submission until the user picks a value.
<form method="POST">
<Combobox name="framework" items={frameworks} required>
...
</Combobox>
<button type="submit">Submit</button>
</form>Basic
A simple combobox with a list of frameworks.
Multiple
A combobox with multiple selection using multiple and ComboboxChips.
Clear Button
Use the showClear prop to show a clear button.
Groups
Use ComboboxGroup and ComboboxSeparator to group items.
Custom Items
You can render a custom component inside ComboboxItem.
Invalid
Use the aria-invalid prop to make the combobox invalid.
Disabled
Use the disabled prop to disable the combobox.
Auto Highlight
Use the autoHighlight prop to automatically highlight the first item on filter.
Popup
You can trigger the combobox from a button or any other component by using the render snippet of ComboboxTrigger. Move the ComboboxInput inside the ComboboxContent.
Input Group
You can add an addon to the combobox by using the InputGroupAddon component inside the ComboboxInput.
RTL
Wrap the combobox in a DirectionProvider and set dir on ComboboxContent. Use the buttons to switch the language.
Examples
API reference
Combobox is styled on top of the Sveltery Base Combobox primitive, which owns the filtering, popup positioning, keyboard navigation, focus handling, dismissal and form value. ComboboxContent renders the Base portal, positioner and popup, and takes the positioner's side, align, sideOffset, alignOffset and anchor props. ComboboxInput takes showTrigger and showClear, and ComboboxChip takes showRemove. The trigger and the input group button set data-popup-open and data-pressed while the popup is open, so style the open state with data-popup-open:. See the Base UI Combobox documentation for the behavior it ports.