# CommandMenu

> A modal search palette over a flat vocabulary of items — grouped results, diacritic-insensitive filtering, match highlighting, and full keyboard navigation with focus that never leaves the search field.

- Package: `fancy-ui-svelte` (Svelte 5 + Tailwind CSS v4)
- Category: Navigation
- Tags: command, palette, search, modal, listbox, accessibility
- Docs: https://fancy-ui.rama.app/docs/components/command-menu
- Source: https://github.com/RamaHerbin/fancy-ui/tree/main/src/lib/fancy-ui/command-menu

## Import

```svelte
<script lang="ts">
  import { CommandMenu } 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 { CommandMenu } from 'fancy-ui-react';` (same prop names; `class` becomes `className`, snippets become `ReactNode`).

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | `false` | Whether the menu is open; bindable |
| `onOpenChange` | `(open: boolean) => void` | — | Fires whenever open changes — Escape, an outside click, or committing an item |
| `items` (required) | `CommandItem[]` | — | The full, unfiltered vocabulary |
| `query` | `string` | `""` | The current search text; bindable. Reset every time the menu reopens |
| `onQueryChange` | `(query: string) => void` | — | Fires whenever query changes |
| `onSelect` | `(item: CommandItem) => void` | — | Called with the committed item, after that item's own onSelect |
| `placeholder` | `string` | `"Search..."` | Placeholder for the search field |
| `emptyMessage` | `string` | `"No results"` | Shown in place of the list when nothing matches |
| `label` | `string` | `"Command menu"` | Accessible name for the dialog and the search field |
| `filter` | `(item: CommandItem, query: string) => boolean` | — | Overrides the default case- and diacritic-insensitive substring filter entirely |
| `class` | `string` | — | Additional CSS classes for the panel |
| `ref` | `HTMLDivElement \| null` | `null` | Bindable reference to the panel element. |
| `sound` | `boolean` | `false` | Plays select on a committed row and close on a dismissal, once the user has enabled sound |

## Snippets

| Snippet | Description |
| --- | --- |
| `icon` | Rendered before each row's label, given that row's item. Treated as decorative |
| `empty` | Rendered in place of the list when nothing matches, instead of emptyMessage |

## Examples

### Basic Usage

```svelte
<script lang="ts">
	import { CommandMenu, type CommandItem } from "fancy-ui-svelte";
	import { Button } from "fancy-ui-svelte";

	let open = $state(false);
	let picked = $state("");

	const items: CommandItem[] = [
		{ id: "button", label: "Button", meta: "Actions" },
		{ id: "icon-button", label: "Icon Button", meta: "Actions" },
		{ id: "rainbow-button", label: "Rainbow Button", meta: "Buttons" },
		{ id: "settings", label: "Settings" },
	];

	function handleSelect(item: CommandItem) {
		picked = item.label;
	}
</script>

<div class="flex flex-col items-start gap-3">
	<Button onclick={() => (open = true)}>Open command menu</Button>
	{#if picked}
		<p class="text-muted-foreground text-[12px]">Last picked: {picked}</p>
	{/if}
</div>

<CommandMenu sound bind:open {items} onSelect={handleSelect} />
```

### Grouped Items

Items with no group render first, then each group under its own heading.

```svelte
<script lang="ts">
	import { CommandMenu, type CommandItem } from "fancy-ui-svelte";
	import { Button } from "fancy-ui-svelte";

	let open = $state(false);

	// Items with no `group` render first, ungrouped — "Search everything"
	// here. Everything else buckets under the uppercase heading matching its
	// own `group`, in the order that group name first appears in the list.
	const items: CommandItem[] = [
		{ id: "search", label: "Search everything" },
		{ id: "new-doc", label: "New document", group: "Create", meta: "Document" },
		{ id: "new-folder", label: "New folder", group: "Create", meta: "Folder" },
		{ id: "profile", label: "Go to profile", group: "Navigate" },
		{ id: "billing", label: "Go to billing", group: "Navigate" },
	];
</script>

<Button onclick={() => (open = true)}>Open command menu</Button>

<CommandMenu sound bind:open {items} label="Jump to" />
```

### Empty State

The message shown when nothing matches the query.

```svelte
<script lang="ts">
	import { CommandMenu, type CommandItem } from "fancy-ui-svelte";
	import { Button } from "fancy-ui-svelte";

	let open = $state(false);

	const items: CommandItem[] = [
		{ id: "button", label: "Button" },
		{ id: "icon-button", label: "Icon Button" },
		{ id: "rainbow-button", label: "Rainbow Button" },
	];
</script>

<!--
	Opens on a click, with the query pre-filled to something no item matches, so
	the empty state is one interaction away rather than a keystroke away.

	It deliberately does NOT mount already-open. `CommandMenu` is modal and
	portalled to `document.body`, so an example rendered with `open` set does
	not appear inside the docs page's preview box the way every non-modal
	example does — it covers the entire page, traps focus and locks scrolling
	from the moment the route mounts, before the reader has scrolled anywhere
	near the Examples section. Whatever the example is meant to demonstrate,
	that is what a visitor sees first.
-->
<Button onclick={() => (open = true)}>Show the empty state</Button>

<CommandMenu
	sound
	bind:open
	{items}
	query="zzz-no-match"
	emptyMessage="No components match “zzz-no-match”."
/>
```

### Custom Filter

Replacing the default filter entirely, e.g. with a prefix match.

```svelte
<script lang="ts">
	import { CommandMenu, type CommandItem } from "fancy-ui-svelte";
	import { Button } from "fancy-ui-svelte";

	let open = $state(false);

	const items: CommandItem[] = [
		{ id: "button", label: "Button" },
		{ id: "badge", label: "Badge" },
		{ id: "banner", label: "Banner" },
		{ id: "icon-button", label: "Icon Button" },
	];

	// Replaces the default filter entirely — only a prefix match on the
	// label counts, so typing "ba" surfaces "Badge" and "Banner" but not
	// "Icon Button", even though "icon-button" never appears anywhere here.
	function prefixFilter(item: CommandItem, query: string): boolean {
		return item.label.toLowerCase().startsWith(query.toLowerCase());
	}
</script>

<Button onclick={() => (open = true)}>Open command menu</Button>

<CommandMenu sound bind:open {items} filter={prefixFilter} />
```

### With Keywords

Matching on keywords that never appear in the visible label.

```svelte
<script lang="ts">
	import { CommandMenu, type CommandItem } from "fancy-ui-svelte";
	import { Button } from "fancy-ui-svelte";

	let open = $state(false);

	// `keywords` extend what the default filter matches against without
	// ever being shown — typing "invoice" or "plan" still finds "Billing",
	// and typing "shortcut" still finds "Keyboard Shortcuts".
	const items: CommandItem[] = [
		{ id: "billing", label: "Billing", meta: "Settings", keywords: ["invoice", "plan", "payment"] },
		{
			id: "shortcuts",
			label: "Keyboard Shortcuts",
			meta: "Help",
			keywords: ["hotkeys", "bindings"],
		},
		{ id: "theme", label: "Appearance", meta: "Settings", keywords: ["dark mode", "light mode"] },
	];
</script>

<Button onclick={() => (open = true)}>Open command menu</Button>

<CommandMenu sound bind:open {items} placeholder="Try “invoice” or “hotkeys”…" />
```
