Combobox

Autocomplete input and command palette with a list of suggestions.

Loading preview…

Installation

Usage

Combobox is a composition of Popover and Command (plus Button for the trigger). Install those primitives, then compose them for searchable select UX.

1<script lang="ts">
2	import CheckIcon from '@lucide/svelte/icons/check';
3	import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
4	import { tick } from 'svelte';
5	import { cn } from '$lib/utils';
6	import Button from '~/components/button/button.svelte';
7	import * as Command from '~/components/command';
8	import * as Popover from '~/components/popover';
9
10	const frameworks = [
11		{ value: 'sveltekit', label: 'SvelteKit' },
12		{ value: 'next.js', label: 'Next.js' },
13		{ value: 'nuxt.js', label: 'Nuxt.js' },
14		{ value: 'remix', label: 'Remix' },
15		{ value: 'astro', label: 'Astro' }
16	];
17
18	let open = $state(false);
19	let value = $state('');
20	let triggerRef = $state<HTMLButtonElement>(null!);
21
22	const selectedValue = $derived(frameworks.find((f) => f.value === value)?.label);
23
24	function closeAndFocusTrigger() {
25		open = false;
26		tick().then(() => {
27			triggerRef.focus();
28		});
29	}
30</script>
31
32<Popover.Root bind:open>
33	<Popover.Trigger bind:ref={triggerRef}>
34		{#snippet child({ props })}
35			<Button
36				{...props}
37				variant="outline"
38				class="w-[200px] justify-between"
39				role="combobox"
40				aria-expanded={open}
41			>
42				{selectedValue || 'Select a framework...'}
43				<ChevronsUpDownIcon class="opacity-50" />
44			</Button>
45		{/snippet}
46	</Popover.Trigger>
47	<Popover.Content class="w-[200px] p-0">
48		<Command.Root>
49			<Command.Input placeholder="Search framework..." />
50			<Command.List>
51				<Command.Empty>No framework found.</Command.Empty>
52				<Command.Group>
53					{#each frameworks as framework (framework.value)}
54						<Command.Item
55							value={framework.value}
56							onSelect={() => {
57								value = framework.value;
58								closeAndFocusTrigger();
59							}}
60						>
61							<CheckIcon class={cn(value !== framework.value && 'text-transparent')} />
62							{framework.label}
63						</Command.Item>
64					{/each}
65				</Command.Group>
66			</Command.List>
67		</Command.Root>
68	</Popover.Content>
69</Popover.Root>