Combobox
Autocomplete input and command palette with a list of suggestions.
Loading preview…
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.js';
6 import Button from '../button/button.svelte';
7 import * as Command from '../command/index.js';
8 import * as Popover from '../popover/index.js';
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>
70Installation
1. Install dependencies
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>