8.0k

Combobox

Autocomplete input with a list of suggestions.

Epicenter

Local-first, open source apps

Special Sponsor
<script lang="ts">
	import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
	import { tick } from "svelte";
	import * as Command from "$lib/components/ui/command/index.js";
	import * as Popover from "$lib/components/ui/popover/index.js";
	import { Button } from "$lib/components/ui/button/index.js";
 
	const frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"];
 
	let open = $state(false);
	let value = $state("");
	let triggerRef = $state<HTMLButtonElement>(null!);
 
	// We want to refocus the trigger button when the user selects
	// an item from the list so users can continue navigating the
	// rest of the form with the keyboard.
	function closeAndFocusTrigger() {
		open = false;
		tick().then(() => {
			triggerRef.focus();
		});
	}
</script>
 
<Popover.Root bind:open>
	<Popover.Trigger bind:ref={triggerRef}>
		{#snippet child({ props })}
			<Button
				{...props}
				variant="outline"
				class="w-[200px] justify-between font-normal"
				role="combobox"
				aria-expanded={open}
			>
				{value || "Select a framework"}
				<ChevronDownIcon class="text-muted-foreground" />
			</Button>
		{/snippet}
	</Popover.Trigger>
	<Popover.Content class="w-[200px] p-0">
		<Command.Root>
			<Command.Input placeholder="Search framework..." />
			<Command.List>
				<Command.Empty>No items found.</Command.Empty>
				<Command.Group>
					{#each frameworks as framework (framework)}
						<Command.Item
							value={framework}
							data-checked={value === framework}
							onSelect={() => {
								value = framework;
								closeAndFocusTrigger();
							}}
						>
							{framework}
						</Command.Item>
					{/each}
				</Command.Group>
			</Command.List>
		</Command.Root>
	</Popover.Content>
</Popover.Root>

Installation

The Combobox is built using a composition of the <Popover /> and the <Command /> components.

See installation instructions for the Popover and the Command components.

Usage

lib/components/example-combobox.svelte
<script lang="ts">
  import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
  import { tick } from "svelte";
  import * as Command from "$lib/components/ui/command/index.js";
  import * as Popover from "$lib/components/ui/popover/index.js";
  import { Button } from "$lib/components/ui/button/index.js";
 
  const frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"];
 
  let open = $state(false);
  let value = $state("");
  let triggerRef = $state<HTMLButtonElement>(null!);
 
  // We want to refocus the trigger button when the user selects
  // an item from the list so users can continue navigating the
  // rest of the form with the keyboard.
  function closeAndFocusTrigger() {
    open = false;
    tick().then(() => {
      triggerRef.focus();
    });
  }
</script>
 
<Popover.Root bind:open>
  <Popover.Trigger bind:ref={triggerRef}>
    {#snippet child({ props })}
      <Button
        {...props}
        variant="outline"
        class="w-[200px] justify-between font-normal"
        role="combobox"
        aria-expanded={open}
      >
        {value || "Select a framework"}
        <ChevronDownIcon class="text-muted-foreground" />
      </Button>
    {/snippet}
  </Popover.Trigger>
  <Popover.Content class="w-[200px] p-0">
    <Command.Root>
      <Command.Input placeholder="Search framework..." />
      <Command.List>
        <Command.Empty>No items found.</Command.Empty>
        <Command.Group>
          {#each frameworks as framework (framework)}
            <Command.Item
              value={framework}
              data-checked={value === framework}
              onSelect={() => {
                value = framework;
                closeAndFocusTrigger();
              }}
            >
              {framework}
            </Command.Item>
          {/each}
        </Command.Group>
      </Command.List>
    </Command.Root>
  </Popover.Content>
</Popover.Root>

Basic

A simple combobox with a list of frameworks.

<script lang="ts">
	import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
	import { tick } from "svelte";
	import * as Command from "$lib/components/ui/command/index.js";
	import * as Popover from "$lib/components/ui/popover/index.js";
	import { Button } from "$lib/components/ui/button/index.js";
 
	const frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"];
 
	let open = $state(false);
	let value = $state("");
	let triggerRef = $state<HTMLButtonElement>(null!);
 
	// We want to refocus the trigger button when the user selects
	// an item from the list so users can continue navigating the
	// rest of the form with the keyboard.
	function closeAndFocusTrigger() {
		open = false;
		tick().then(() => {
			triggerRef.focus();
		});
	}
</script>
 
<Popover.Root bind:open>
	<Popover.Trigger bind:ref={triggerRef}>
		{#snippet child({ props })}
			<Button
				{...props}
				variant="outline"
				class="w-[200px] justify-between font-normal"
				role="combobox"
				aria-expanded={open}
			>
				{value || "Select a framework"}
				<ChevronDownIcon class="text-muted-foreground" />
			</Button>
		{/snippet}
	</Popover.Trigger>
	<Popover.Content class="w-[200px] p-0">
		<Command.Root>
			<Command.Input placeholder="Search framework..." />
			<Command.List>
				<Command.Empty>No items found.</Command.Empty>
				<Command.Group>
					{#each frameworks as framework (framework)}
						<Command.Item
							value={framework}
							data-checked={value === framework}
							onSelect={() => {
								value = framework;
								closeAndFocusTrigger();
							}}
						>
							{framework}
						</Command.Item>
					{/each}
				</Command.Group>
			</Command.List>
		</Command.Root>
	</Popover.Content>
</Popover.Root>

Groups

Use <Command.Group /> and <Command.Separator /> to group items.

<script lang="ts">
	import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
	import { tick } from "svelte";
	import * as Command from "$lib/components/ui/command/index.js";
	import * as Popover from "$lib/components/ui/popover/index.js";
	import { Button } from "$lib/components/ui/button/index.js";
 
	const timezones = [
		{
			value: "Americas",
			items: [
				"(GMT-5) New York",
				"(GMT-8) Los Angeles",
				"(GMT-6) Chicago",
				"(GMT-5) Toronto",
				"(GMT-8) Vancouver",
				"(GMT-3) São Paulo",
			],
		},
		{
			value: "Europe",
			items: [
				"(GMT+0) London",
				"(GMT+1) Paris",
				"(GMT+1) Berlin",
				"(GMT+1) Rome",
				"(GMT+1) Madrid",
				"(GMT+1) Amsterdam",
			],
		},
		{
			value: "Asia/Pacific",
			items: [
				"(GMT+9) Tokyo",
				"(GMT+8) Shanghai",
				"(GMT+8) Singapore",
				"(GMT+4) Dubai",
				"(GMT+11) Sydney",
				"(GMT+9) Seoul",
			],
		},
	];
 
	let open = $state(false);
	let value = $state("");
	let triggerRef = $state<HTMLButtonElement>(null!);
 
	// We want to refocus the trigger button when the user selects
	// an item from the list so users can continue navigating the
	// rest of the form with the keyboard.
	function closeAndFocusTrigger() {
		open = false;
		tick().then(() => {
			triggerRef.focus();
		});
	}
</script>
 
<Popover.Root bind:open>
	<Popover.Trigger bind:ref={triggerRef}>
		{#snippet child({ props })}
			<Button
				{...props}
				variant="outline"
				class="w-60 justify-between font-normal"
				role="combobox"
				aria-expanded={open}
			>
				{value || "Select a timezone"}
				<ChevronDownIcon class="text-muted-foreground" />
			</Button>
		{/snippet}
	</Popover.Trigger>
	<Popover.Content class="w-60 p-0">
		<Command.Root>
			<Command.Input placeholder="Search timezone..." />
			<Command.List>
				<Command.Empty>No timezones found.</Command.Empty>
				{#each timezones as group, index (group.value)}
					<Command.Group heading={group.value}>
						{#each group.items as item (item)}
							<Command.Item
								value={item}
								data-checked={value === item}
								onSelect={() => {
									value = item;
									closeAndFocusTrigger();
								}}
							>
								{item}
							</Command.Item>
						{/each}
					</Command.Group>
					{#if index < timezones.length - 1}
						<Command.Separator />
					{/if}
				{/each}
			</Command.List>
		</Command.Root>
	</Popover.Content>
</Popover.Root>

Custom Items

You can render a custom component inside <Command.Item />.

<script lang="ts">
	import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
	import { tick } from "svelte";
	import * as Command from "$lib/components/ui/command/index.js";
	import * as Item from "$lib/components/ui/item/index.js";
	import * as Popover from "$lib/components/ui/popover/index.js";
	import { Button } from "$lib/components/ui/button/index.js";
 
	const countries = [
		{ code: "", value: "", continent: "", label: "Select country" },
		{ code: "ar", value: "argentina", label: "Argentina", continent: "South America" },
		{ code: "au", value: "australia", label: "Australia", continent: "Oceania" },
		{ code: "br", value: "brazil", label: "Brazil", continent: "South America" },
		{ code: "ca", value: "canada", label: "Canada", continent: "North America" },
		{ code: "cn", value: "china", label: "China", continent: "Asia" },
		{ code: "co", value: "colombia", label: "Colombia", continent: "South America" },
		{ code: "eg", value: "egypt", label: "Egypt", continent: "Africa" },
		{ code: "fr", value: "france", label: "France", continent: "Europe" },
		{ code: "de", value: "germany", label: "Germany", continent: "Europe" },
		{ code: "it", value: "italy", label: "Italy", continent: "Europe" },
		{ code: "jp", value: "japan", label: "Japan", continent: "Asia" },
		{ code: "ke", value: "kenya", label: "Kenya", continent: "Africa" },
		{ code: "mx", value: "mexico", label: "Mexico", continent: "North America" },
		{ code: "nz", value: "new-zealand", label: "New Zealand", continent: "Oceania" },
		{ code: "ng", value: "nigeria", label: "Nigeria", continent: "Africa" },
		{ code: "za", value: "south-africa", label: "South Africa", continent: "Africa" },
		{ code: "kr", value: "south-korea", label: "South Korea", continent: "Asia" },
		{ code: "gb", value: "united-kingdom", label: "United Kingdom", continent: "Europe" },
		{ code: "us", value: "united-states", label: "United States", continent: "North America" },
	];
 
	const items = countries.filter((country) => country.code !== "");
 
	let open = $state(false);
	let value = $state("");
	let triggerRef = $state<HTMLButtonElement>(null!);
 
	// We want to refocus the trigger button when the user selects
	// an item from the list so users can continue navigating the
	// rest of the form with the keyboard.
	function closeAndFocusTrigger() {
		open = false;
		tick().then(() => {
			triggerRef.focus();
		});
	}
</script>
 
<Popover.Root bind:open>
	<Popover.Trigger bind:ref={triggerRef}>
		{#snippet child({ props })}
			<Button
				{...props}
				variant="outline"
				class="w-[200px] justify-between font-normal"
				role="combobox"
				aria-expanded={open}
			>
				{items.find((country) => country.value === value)?.label || "Select country"}
				<ChevronDownIcon class="text-muted-foreground" />
			</Button>
		{/snippet}
	</Popover.Trigger>
	<Popover.Content class="w-[200px] p-0">
		<Command.Root>
			<Command.Input placeholder="Search countries..." />
			<Command.List>
				<Command.Empty>No countries found.</Command.Empty>
				<Command.Group>
					{#each items as country (country.code)}
						<Command.Item
							value={country.label}
							data-checked={value === country.value}
							onSelect={() => {
								value = country.value;
								closeAndFocusTrigger();
							}}
						>
							<Item.Root size="xs" class="p-0">
								<Item.Content>
									<Item.Title class="whitespace-nowrap">{country.label}</Item.Title>
									<Item.Description>
										{country.continent} ({country.code})
									</Item.Description>
								</Item.Content>
							</Item.Root>
						</Command.Item>
					{/each}
				</Command.Group>
			</Command.List>
		</Command.Root>
	</Popover.Content>
</Popover.Root>

Invalid

Use the aria-invalid prop to make the combobox invalid.

<script lang="ts">
	import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
	import { tick } from "svelte";
	import * as Command from "$lib/components/ui/command/index.js";
	import * as Popover from "$lib/components/ui/popover/index.js";
	import { Button } from "$lib/components/ui/button/index.js";
 
	const frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"];
 
	let open = $state(false);
	let value = $state("");
	let triggerRef = $state<HTMLButtonElement>(null!);
 
	// We want to refocus the trigger button when the user selects
	// an item from the list so users can continue navigating the
	// rest of the form with the keyboard.
	function closeAndFocusTrigger() {
		open = false;
		tick().then(() => {
			triggerRef.focus();
		});
	}
</script>
 
<Popover.Root bind:open>
	<Popover.Trigger bind:ref={triggerRef}>
		{#snippet child({ props })}
			<Button
				{...props}
				variant="outline"
				class="w-[200px] justify-between font-normal"
				role="combobox"
				aria-expanded={open}
				aria-invalid="true"
			>
				{value || "Select a framework"}
				<ChevronDownIcon class="text-muted-foreground" />
			</Button>
		{/snippet}
	</Popover.Trigger>
	<Popover.Content class="w-[200px] p-0">
		<Command.Root>
			<Command.Input placeholder="Search framework..." />
			<Command.List>
				<Command.Empty>No items found.</Command.Empty>
				<Command.Group>
					{#each frameworks as framework (framework)}
						<Command.Item
							value={framework}
							data-checked={value === framework}
							onSelect={() => {
								value = framework;
								closeAndFocusTrigger();
							}}
						>
							{framework}
						</Command.Item>
					{/each}
				</Command.Group>
			</Command.List>
		</Command.Root>
	</Popover.Content>
</Popover.Root>

Disabled

Use the disabled prop to disable the combobox.

<script lang="ts">
	import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
	import { tick } from "svelte";
	import * as Command from "$lib/components/ui/command/index.js";
	import * as Popover from "$lib/components/ui/popover/index.js";
	import { Button } from "$lib/components/ui/button/index.js";
 
	const frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"];
 
	let open = $state(false);
	let value = $state("");
	let triggerRef = $state<HTMLButtonElement>(null!);
 
	// We want to refocus the trigger button when the user selects
	// an item from the list so users can continue navigating the
	// rest of the form with the keyboard.
	function closeAndFocusTrigger() {
		open = false;
		tick().then(() => {
			triggerRef.focus();
		});
	}
</script>
 
<Popover.Root bind:open>
	<Popover.Trigger bind:ref={triggerRef} disabled>
		{#snippet child({ props })}
			<Button
				{...props}
				variant="outline"
				class="w-[200px] justify-between font-normal"
				role="combobox"
				aria-expanded={open}
			>
				{value || "Select a framework"}
				<ChevronDownIcon class="text-muted-foreground" />
			</Button>
		{/snippet}
	</Popover.Trigger>
	<Popover.Content class="w-[200px] p-0">
		<Command.Root>
			<Command.Input placeholder="Search framework..." />
			<Command.List>
				<Command.Empty>No items found.</Command.Empty>
				<Command.Group>
					{#each frameworks as framework (framework)}
						<Command.Item
							value={framework}
							data-checked={value === framework}
							onSelect={() => {
								value = framework;
								closeAndFocusTrigger();
							}}
						>
							{framework}
						</Command.Item>
					{/each}
				</Command.Group>
			</Command.List>
		</Command.Root>
	</Popover.Content>
</Popover.Root>

You can trigger the combobox from a button or any other component by using the child snippet on <Popover.Trigger />. Place the <Command.Input /> inside the <Popover.Content />.

<script lang="ts">
	import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
	import { tick } from "svelte";
	import * as Command from "$lib/components/ui/command/index.js";
	import * as Popover from "$lib/components/ui/popover/index.js";
	import { Button } from "$lib/components/ui/button/index.js";
 
	const countries = [
		{ code: "", value: "", continent: "", label: "Select country" },
		{ code: "ar", value: "argentina", label: "Argentina", continent: "South America" },
		{ code: "au", value: "australia", label: "Australia", continent: "Oceania" },
		{ code: "br", value: "brazil", label: "Brazil", continent: "South America" },
		{ code: "ca", value: "canada", label: "Canada", continent: "North America" },
		{ code: "cn", value: "china", label: "China", continent: "Asia" },
		{ code: "co", value: "colombia", label: "Colombia", continent: "South America" },
		{ code: "eg", value: "egypt", label: "Egypt", continent: "Africa" },
		{ code: "fr", value: "france", label: "France", continent: "Europe" },
		{ code: "de", value: "germany", label: "Germany", continent: "Europe" },
		{ code: "it", value: "italy", label: "Italy", continent: "Europe" },
		{ code: "jp", value: "japan", label: "Japan", continent: "Asia" },
		{ code: "ke", value: "kenya", label: "Kenya", continent: "Africa" },
		{ code: "mx", value: "mexico", label: "Mexico", continent: "North America" },
		{ code: "nz", value: "new-zealand", label: "New Zealand", continent: "Oceania" },
		{ code: "ng", value: "nigeria", label: "Nigeria", continent: "Africa" },
		{ code: "za", value: "south-africa", label: "South Africa", continent: "Africa" },
		{ code: "kr", value: "south-korea", label: "South Korea", continent: "Asia" },
		{ code: "gb", value: "united-kingdom", label: "United Kingdom", continent: "Europe" },
		{ code: "us", value: "united-states", label: "United States", continent: "North America" },
	];
 
	let open = $state(false);
	let value = $state(countries[0].label);
	let triggerRef = $state<HTMLButtonElement>(null!);
 
	// We want to refocus the trigger button when the user selects
	// an item from the list so users can continue navigating the
	// rest of the form with the keyboard.
	function closeAndFocusTrigger() {
		open = false;
		tick().then(() => {
			triggerRef.focus();
		});
	}
</script>
 
<Popover.Root bind:open>
	<Popover.Trigger bind:ref={triggerRef}>
		{#snippet child({ props })}
			<Button
				{...props}
				variant="outline"
				class="w-64 justify-between font-normal"
				role="combobox"
				aria-expanded={open}
			>
				{value}
				<ChevronDownIcon class="text-muted-foreground" />
			</Button>
		{/snippet}
	</Popover.Trigger>
	<Popover.Content class="w-64 p-0">
		<Command.Root>
			<Command.Input placeholder="Search" />
			<Command.List>
				<Command.Empty>No items found.</Command.Empty>
				<Command.Group>
					{#each countries as country (country.code)}
						<Command.Item
							value={country.label}
							data-checked={value === country.label}
							onSelect={() => {
								value = country.label;
								closeAndFocusTrigger();
							}}
						>
							{country.label}
						</Command.Item>
					{/each}
				</Command.Group>
			</Command.List>
		</Command.Root>
	</Popover.Content>
</Popover.Root>