# Combobox

> A single-choice text field over a closed set of options — typing filters a portalled listbox, and a value outside the list can never be selected.

- Package: `fancy-ui-svelte` (Svelte 5 + Tailwind CSS v4)
- Category: Forms
- Tags: combobox, dropdown, listbox, filter, form, compound, accessibility
- Docs: https://fancy-ui.rama.app/docs/components/combobox
- Source: https://github.com/RamaHerbin/fancy-ui/tree/main/src/lib/fancy-ui/combobox

## Import

```svelte
<script lang="ts">
  import { Combobox } from 'fancy-ui-svelte';
</script>
```

Setup (once per project): `npm install fancy-ui-svelte`, then add `@import "fancy-ui-svelte/tailwind.css";` after `@import "tailwindcss";` in your main CSS file. Full guide: https://fancy-ui.rama.app/llms.txt

React: `import { Combobox } from 'fancy-ui-react';` (same prop names; `class` becomes `className`, snippets become `ReactNode`).

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `options` (required) | `ComboboxOption[]` | — | The closed set of selectable options. |
| `value` | `string` | `""` | The selected option's value, bindable — "" means nothing is selected. |
| `onValueChange` | `(value: string) => void` | — | Called whenever the selection commits. |
| `placeholder` | `string` | — | Shown while the field is empty and nothing is selected. |
| `disabled` | `boolean` | `false` | Blocks focus and typing; excluded from form submission. |
| `required` | `boolean` | `false` | Native required, on the visible field. |
| `invalid` | `boolean` | `false` | Drives the error border and aria-invalid. |
| `id` | `string` | — | Element id. |
| `name` | `string` | — | Native name — submitted via a hidden input carrying value, not the visible label. |
| `label` | `string` | — | Accessible name — for a control with no visible Label next to it. |
| `filter` | `(option: ComboboxOption, query: string) => boolean` | — | Matches an option against the current query. Default: case-insensitive substring on label. |
| `emptyMessage` | `string` | `"No results"` | Shown in the panel when no option matches the current query. |
| `class` | `string` | — | Additional CSS classes. |
| `ref` | `HTMLInputElement \| null` | `null` | Bindable reference to the input element. |
| `sound` | `boolean` | `false` | Plays the select cue when an option is committed, once the user has enabled sound |

## Examples

### Basic Usage

Four frameworks, filtered by typing.

```svelte
<script lang="ts">
	import { Combobox } from "fancy-ui-svelte";

	const frameworks = [
		{ value: "svelte-5", label: "Svelte 5" },
		{ value: "sveltekit", label: "SvelteKit" },
		{ value: "react", label: "React" },
		{ value: "vue", label: "Vue" },
	];

	let framework = $state("");
</script>

<Combobox
	sound
	options={frameworks}
	bind:value={framework}
	label="Framework"
	placeholder="Search…"
/>
```

### Custom Filter

Matches by value prefix instead of the label — shows the highlight degrading gracefully when the match isn't a literal label substring.

```svelte
<script lang="ts">
	// The default filter matches substrings of the label. This one matches
	// only a prefix of the *value* instead, so typing the short slug ("sk")
	// finds "SvelteKit" even though "sk" never appears in its label.
	import { Combobox, type ComboboxOption } from "fancy-ui-svelte";

	const frameworks: ComboboxOption[] = [
		{ value: "svelte-5", label: "Svelte 5" },
		{ value: "sk", label: "SvelteKit" },
		{ value: "react", label: "React" },
		{ value: "vue", label: "Vue" },
	];

	let framework = $state("");

	function filterByValuePrefix(option: ComboboxOption, query: string): boolean {
		return option.value.toLowerCase().startsWith(query.toLowerCase());
	}
</script>

<Combobox
	sound
	options={frameworks}
	bind:value={framework}
	filter={filterByValuePrefix}
	label="Framework (matched by slug)"
	placeholder="Try “sk”…"
/>
```

### With FormField

Wrapped in FormField — required and the error message driven by context.

```svelte
<script lang="ts">
	// FormField supplies controlId, aria-describedby and aria-invalid to
	// Combobox through context — no id or invalid prop needed here.
	import { FormField } from "fancy-ui-svelte";
	import { Combobox } from "fancy-ui-svelte";

	const frameworks = [
		{ value: "svelte-5", label: "Svelte 5" },
		{ value: "sveltekit", label: "SvelteKit" },
		{ value: "react", label: "React" },
		{ value: "vue", label: "Vue" },
	];

	let framework = $state("");
</script>

<FormField
	label="Framework"
	required
	error={framework ? undefined : "Pick a framework before continuing."}
	class="w-full max-w-sm"
>
	<Combobox sound options={frameworks} bind:value={framework} />
</FormField>
```
