About
Command is built on @sveltery/command, a Svelte port of cmdk by Paco Coursey. The port is released under the MIT License and has no runtime dependencies.
Installation
Install the dependencies
pnpm add cn class-variance-authority @sveltery/command github:sveltery/baseCopy the component
Copy the following files into src/lib/components/ui/command.
It imports the dialog and input-group components, so copy their folders too.
<script lang="ts">
import type { ComponentProps } from 'svelte';
import { Root } from '@sveltery/command';
import { cn } from 'cn';
let { value = $bindable(''), class: classProp, ...props }: ComponentProps<typeof Root> = $props();
</script>
<Root data-slot="command" bind:value class={cn('bg-popover text-popover-foreground rounded-xl! p-1 flex size-full flex-col overflow-hidden', classProp)} {...props} /><script lang="ts">
import type { ComponentProps, Snippet } from 'svelte';
import { cn } from 'cn';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../dialog/index.js';
let {
open = $bindable(),
title = 'Command Palette',
description = 'Search for a command to run...',
class: classProp,
showCloseButton = false,
children,
...props
}: Omit<ComponentProps<typeof Dialog>, 'children'> & {
title?: string;
description?: string;
class?: string;
showCloseButton?: boolean;
children: Snippet;
} = $props();
</script>
<Dialog bind:open {...props}>
<DialogHeader class="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent class={cn('rounded-xl! top-1/3 translate-y-0 overflow-hidden p-0', classProp)} {showCloseButton}>
{@render children()}
</DialogContent>
</Dialog><script lang="ts">
import type { ComponentProps } from 'svelte';
import { Empty } from '@sveltery/command';
import { cn } from 'cn';
let { class: classProp, ...props }: ComponentProps<typeof Empty> = $props();
</script>
<Empty data-slot="command-empty" class={cn('py-6 text-center text-sm', classProp)} {...props} /><script lang="ts">
import type { ComponentProps } from 'svelte';
import { Group } from '@sveltery/command';
import { cn } from 'cn';
let { class: classProp, ...props }: ComponentProps<typeof Group> = $props();
</script>
<Group data-slot="command-group" class={cn('text-foreground **:[[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium', classProp)} {...props} /><script lang="ts">
import type { ComponentProps } from 'svelte';
import { Input } from '@sveltery/command';
import { cn } from 'cn';
import { InputGroup, InputGroupAddon } from '../input-group/index.js';
import SearchIcon from '@lucide/svelte/icons/search';
let { value = $bindable(''), class: classProp, ...props }: ComponentProps<typeof Input> = $props();
</script>
<div data-slot="command-input-wrapper" class="p-1 pb-0">
<InputGroup class="bg-input/30 border-input/30 h-8! rounded-lg! shadow-none! *:data-[slot=input-group-addon]:pl-2!">
<Input data-slot="command-input" bind:value class={cn('w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50', classProp)} {...props} />
<InputGroupAddon>
<SearchIcon class="size-4 shrink-0 opacity-50" />
</InputGroupAddon>
</InputGroup>
</div><script lang="ts">
import type { ComponentProps } from 'svelte';
import { Item } from '@sveltery/command';
import { cn } from 'cn';
import CheckIcon from '@lucide/svelte/icons/check';
let { class: classProp, children, ...props }: ComponentProps<typeof Item> = $props();
</script>
<Item
data-slot="command-item"
class={cn("data-selected:bg-muted data-selected:text-foreground data-selected:*:[svg]:text-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! [&_svg:not([class*='size-'])]:size-4 group/command-item data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", classProp)}
{...props}
>
{@render children?.()}
<CheckIcon
class="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100"
/>
</Item><script lang="ts">
import type { ComponentProps } from 'svelte';
import { List } from '@sveltery/command';
import { cn } from 'cn';
let { class: classProp, ...props }: ComponentProps<typeof List> = $props();
</script>
<List data-slot="command-list" class={cn('no-scrollbar max-h-72 scroll-py-1 outline-none overflow-x-hidden overflow-y-auto', classProp)} {...props} /><script lang="ts">
import type { ComponentProps } from 'svelte';
import { Separator } from '@sveltery/command';
import { cn } from 'cn';
let { class: classProp, ...props }: ComponentProps<typeof Separator> = $props();
</script>
<Separator data-slot="command-separator" class={cn('bg-border -mx-1 h-px', classProp)} {...props} /><script lang="ts">
import { cn } from 'cn';
import type { HTMLAttributes } from 'svelte/elements';
let { children, class: classProp, ...props }: HTMLAttributes<HTMLSpanElement> = $props();
</script>
<span data-slot="command-shortcut" {...props} class={cn('text-muted-foreground group-data-selected/command-item:text-foreground ml-auto text-xs tracking-widest', classProp)}>
{@render children?.()}
</span>export { default as Command } from './Command.svelte';
export { default as CommandDialog } from './CommandDialog.svelte';
export { default as CommandInput } from './CommandInput.svelte';
export { default as CommandList } from './CommandList.svelte';
export { default as CommandEmpty } from './CommandEmpty.svelte';
export { default as CommandGroup } from './CommandGroup.svelte';
export { default as CommandItem } from './CommandItem.svelte';
export { default as CommandShortcut } from './CommandShortcut.svelte';
export { default as CommandSeparator } from './CommandSeparator.svelte';Update the import paths
Change the imports to match your project setup.
Usage
<script lang="ts">
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from '$lib/components/ui/command';
</script><Command class="max-w-sm rounded-lg border">
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem>Calendar</CommandItem>
<CommandItem>Search Emoji</CommandItem>
<CommandItem>Calculator</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Settings">
<CommandItem>Profile</CommandItem>
<CommandItem>Billing</CommandItem>
<CommandItem>Settings</CommandItem>
</CommandGroup>
</CommandList>
</Command>Bind the selected item with bind:value on Command and the search text with bind:value on CommandInput. Run an action when an item is chosen with onSelect on CommandItem.
Composition
Use the following composition to build a Command:
Command
├── CommandInput
└── CommandList
├── CommandEmpty
├── CommandGroup
│ ├── CommandItem
│ └── CommandItem
├── CommandSeparator
└── CommandGroup
├── CommandItem
└── CommandItemFiltering and ranking
Items filter and rank by a fuzzy score as you type. An item that does not match the search is hidden, and the best matches come first.
An item matches on its value, which defaults to its text content. Give an item keywords to match words that are not in its text.
<CommandItem value="billing" keywords={['invoice', 'payment']}>Billing</CommandItem>Pass filter to Command to score items yourself. It receives the item value, the search text and the item keywords, and returns a number. An item that scores 0 is hidden, and a higher score ranks first.
<Command filter={(value, search) => (value.toLowerCase().startsWith(search.toLowerCase()) ? 1 : 0)}>
...
</Command>Set shouldFilter={false} when you filter the items yourself, for example with results from a server. Command then shows every item you render, in the order you render them.
<script lang="ts">
let search = $state('');
let results = $state<string[]>([]);
$effect(() => {
const query = search;
fetch(`/api/search?q=${encodeURIComponent(query)}`)
.then((response) => response.json())
.then((items) => (results = items));
});
</script>
<Command shouldFilter={false}>
<CommandInput bind:value={search} placeholder="Search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
{#each results as result (result)}
<CommandItem value={result}>{result}</CommandItem>
{/each}
</CommandList>
</Command>While a search is active, the menu shows results in ranked order with CSS order. The DOM keeps the source order, so a screen reader in browse mode reads the results in source order. The arrow keys and the announced active option follow the order on screen.
Basic
A simple command menu in a dialog.
Shortcuts
Groups
A command menu with groups, icons and separators.
Scrollable
Scrollable command menu with multiple items.
Dialog
CommandDialog shows a command menu in a dialog. It takes bind:open, and title, description and showCloseButton for the dialog. The title and description are only for screen readers. Put a Command inside it.
Press ⌘J or Ctrl+J to open the menu below. A <svelte:window onkeydown> handler toggles open.
<script lang="ts">
let open = $state(false);
function onkeydown(event: KeyboardEvent) {
if (event.key === 'j' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
open = !open;
}
}
</script>
<svelte:window {onkeydown} />
<CommandDialog bind:open>
<Command>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem>Calendar</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</CommandDialog>RTL
Wrap the menu in a DirectionProvider and set dir on Command and CommandInput. Use the buttons to switch the language.
API reference
The @sveltery/command README documents the parts, the props and the keyboard behavior in full. The tables below summarize the props.
Command
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | Bindable. The value of the selected item | |
shouldFilter | boolean | true | Filters and ranks items by the search. Set false to filter them yourself |
filter | (value, search, keywords) => number | fuzzy score | Scores an item. 0 hides it and a higher score ranks first |
loop | boolean | false | Wraps the selection from the last item to the first, and back |
vimBindings | boolean | true | Moves the selection with Ctrl+N, Ctrl+P, Ctrl+J and Ctrl+K |
label | string | An accessible name for the menu | |
class | string | Added to the menu |
CommandInput
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | '' | Bindable. The search text |
placeholder | string | The input placeholder | |
class | string | Added to the input |
CommandGroup
| Prop | Type | Default | Description |
|---|---|---|---|
heading | string \| Snippet | The group label | |
class | string | Added to the group |
CommandItem
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | text content | What the search matches and what Command reports as selected |
keywords | string[] | Extra words the search matches | |
disabled | boolean | false | Skips the item for selection and keyboard navigation |
forceMount | boolean | false | Keeps the item in the list when the search does not match it |
onSelect | (value: string) => void | Runs when the item is chosen with the pointer or Enter | |
class | string | Added to the item |
CommandDialog
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | false | Bindable. Whether the dialog is open |
title | string | 'Command Palette' | The dialog title, read by screen readers |
description | string | 'Search for a command to run...' | The dialog description, read by screen readers |
showCloseButton | boolean | false | Shows a close button in the dialog |
class | string | Added to the dialog content |
CommandList, CommandEmpty and CommandSeparator take class. CommandShortcut renders a <span> and takes class.