Command

Command menu for search and quick actions.

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/base

Copy 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.

src/lib/components/ui/command/Command.svelte
<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} />
src/lib/components/ui/command/CommandDialog.svelte
<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>
src/lib/components/ui/command/CommandEmpty.svelte
<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} />
src/lib/components/ui/command/CommandGroup.svelte
<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} />
src/lib/components/ui/command/CommandInput.svelte
<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>
src/lib/components/ui/command/CommandItem.svelte
<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>
src/lib/components/ui/command/CommandList.svelte
<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} />
src/lib/components/ui/command/CommandSeparator.svelte
<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} />
src/lib/components/ui/command/CommandShortcut.svelte
<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>
src/lib/components/ui/command/index.ts
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
        └── CommandItem

Filtering 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>

Basic

A simple command menu in a dialog.

Command Palette

Search for a command to run...

Shortcuts

Command Palette

Search for a command to run...

Groups

A command menu with groups, icons and separators.

Command Palette

Search for a command to run...

Scrollable

Scrollable command menu with multiple items.

Command Palette

Search for a command to run...

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.

Press ⌘J

Command Palette

Search for a command to run...

<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

PropTypeDefaultDescription
valuestringBindable. The value of the selected item
shouldFilterbooleantrueFilters and ranks items by the search. Set false to filter them yourself
filter(value, search, keywords) => numberfuzzy scoreScores an item. 0 hides it and a higher score ranks first
loopbooleanfalseWraps the selection from the last item to the first, and back
vimBindingsbooleantrueMoves the selection with Ctrl+N, Ctrl+P, Ctrl+J and Ctrl+K
labelstringAn accessible name for the menu
classstringAdded to the menu

CommandInput

PropTypeDefaultDescription
valuestring''Bindable. The search text
placeholderstringThe input placeholder
classstringAdded to the input

CommandGroup

PropTypeDefaultDescription
headingstring \| SnippetThe group label
classstringAdded to the group

CommandItem

PropTypeDefaultDescription
valuestringtext contentWhat the search matches and what Command reports as selected
keywordsstring[]Extra words the search matches
disabledbooleanfalseSkips the item for selection and keyboard navigation
forceMountbooleanfalseKeeps the item in the list when the search does not match it
onSelect(value: string) => voidRuns when the item is chosen with the pointer or Enter
classstringAdded to the item

CommandDialog

PropTypeDefaultDescription
openbooleanfalseBindable. Whether the dialog is open
titlestring'Command Palette'The dialog title, read by screen readers
descriptionstring'Search for a command to run...'The dialog description, read by screen readers
showCloseButtonbooleanfalseShows a close button in the dialog
classstringAdded to the dialog content

CommandList, CommandEmpty and CommandSeparator take class. CommandShortcut renders a <span> and takes class.