Skip to content
kerfjsGitHub

CommandPalette

Find any action by typing its name.

SourcePackage @kerf-js/veneer

Preview

tsx
import { state } from '@kerf-js/core'import { Button, CommandPalette } from '@kerf-js/veneer'import type { Command } from '@kerf-js/veneer'export function Basic() {  const open = state(false)  return (    <>      <Button        emphasis="outline"        tone="neutral"        icon="search"        onClick={() => (open.value = true)}      >        Search commands      </Button>      <CommandPalette        open={open}        hotkey=""        commands={COMMANDS}      />    </>  )}const COMMANDS: Command[] = [  {    id: 'deploy',    label: 'Deploy storefront',    group: 'Projects',    icon: 'rocket',    shortcut: '⌘D',  },  {    id: 'logs',    label: 'Open logs',    group: 'Projects',    icon: 'external',  },  {    id: 'settings',    label: 'Open settings',    group: 'Go to',    icon: 'settings',    shortcut: '⌘,',  },  {    id: 'invite',    label: 'Invite a teammate',    group: 'Team',    icon: 'user',  },]

Installation

npm install @kerf-js/veneer

Usage

tsx
import { CommandPalette } from '@kerf-js/veneer'

Running a command

onRun gets the command that was picked.

tsx
import { state } from '@kerf-js/core'import {  Button,  CommandPalette,  Stack,  Text} from '@kerf-js/veneer'import type { Command } from '@kerf-js/veneer'export function Run() {  const open = state(false)  const last = state('Nothing yet')  return (    <Stack      gap="related"      align="center"    >      <Button onClick={() => (open.value = true)}>        Open the palette      </Button>      <Text subtle>Last run: {last.value}</Text>      <CommandPalette        open={open}        hotkey=""        commands={COMMANDS}        onRun={(c) => (last.value = c.label)}      />    </Stack>  )}const COMMANDS: Command[] = [  {    id: 'deploy',    label: 'Deploy storefront',    group: 'Projects',    icon: 'rocket',    shortcut: '⌘D',  },  {    id: 'logs',    label: 'Open logs',    group: 'Projects',    icon: 'external',  },  {    id: 'settings',    label: 'Open settings',    group: 'Go to',    icon: 'settings',    shortcut: '⌘,',  },  {    id: 'invite',    label: 'Invite a teammate',    group: 'Team',    icon: 'user',  },]

A list inside a row

A row with children opens its own list. Each child's run does the work.

tsx
import { state } from '@kerf-js/core'import {  Button,  CommandPalette,  Stack,  Text} from '@kerf-js/veneer'import type { Command } from '@kerf-js/veneer'export function Nested() {  const open = state(false)  const region = state('eu-west')  const commands: Command[] = [    {      id: 'region',      label: 'Move storefront to…',      title: 'Region',      group: 'Projects',      icon: 'globe',      children: () =>        REGIONS.map(({ region: r }) => ({          id: r,          label: r,          group: 'Regions',          icon: 'pin',          run: () => (region.value = r),        })),    },    {      id: 'deploy',      label: 'Deploy storefront',      group: 'Projects',      icon: 'rocket',      shortcut: '⌘D',    },  ]  return (    <Stack      gap="related"      align="center"    >      <Button        emphasis="outline"        tone="neutral"        icon="search"        onClick={() => (open.value = true)}      >        Search commands      </Button>      <Text subtle>        storefront runs in {region.value}      </Text>      <CommandPalette        open={open}        hotkey=""        commands={commands}      />    </Stack>  )}const REGIONS = [  { region: 'eu-west', requests: 34.2, share: 40 },  { region: 'us-east', requests: 24.8, share: 29 },  { region: 'ap-south', requests: 12.1, share: 14 },  { region: 'us-west', requests: 9.4, share: 11 },  { region: 'eu-north', requests: 5.6, share: 6 },]

Search as you type

sources are searched after commands. Return a promise and the rows land below, without moving the active row. Try api.

tsx
import { state } from '@kerf-js/core'import {  Button,  CommandPalette,  Stack,  Text} from '@kerf-js/veneer'import type { Command } from '@kerf-js/veneer'export function Search() {  const open = state(false)  const last = state('Nothing yet')  return (    <Stack      gap="related"      align="center"    >      <Button        emphasis="outline"        tone="neutral"        icon="search"        onClick={() => (open.value = true)}      >        Find a project      </Button>      <Text subtle>Opened: {last.value}</Text>      <CommandPalette        open={open}        hotkey=""        placeholder="Search projects, like store or api"        commands={COMMANDS}        sources={[findProjects]}        onRun={(c) => (last.value = c.label)}      />    </Stack>  )}type Status =  | 'Live'  | 'Building'  | 'Failed'  | 'Paused'const PROJECTS: {  id: string  name: string  owner: string  region: string  status: Status  deploys: number  uptime: number  latency: number  cost: number  updated: string}[] = [  {    id: 'storefront',    name: 'storefront',    owner: 'Priya Raman',    region: 'eu-west',    status: 'Live',    deploys: 412,    uptime: 99.98,    latency: 84,    cost: 1240,    updated: '2026-10-31',  },  {    id: 'checkout',    name: 'checkout',    owner: 'Tomás Oliveira',    region: 'eu-west',    status: 'Live',    deploys: 288,    uptime: 99.95,    latency: 112,    cost: 980,    updated: '2026-10-31',  },  {    id: 'search-api',    name: 'search-api',    owner: 'Lena Fischer',    region: 'us-east',    status: 'Building',    deploys: 196,    uptime: 99.91,    latency: 146,    cost: 1610,    updated: '2026-10-30',  },  {    id: 'billing',    name: 'billing',    owner: 'Mara Lindqvist',    region: 'us-east',    status: 'Live',    deploys: 154,    uptime: 99.99,    latency: 73,    cost: 720,    updated: '2026-10-29',  },  {    id: 'mobile-bff',    name: 'mobile-bff',    owner: 'Sam Okafor',    region: 'ap-south',    status: 'Failed',    deploys: 233,    uptime: 99.42,    latency: 208,    cost: 860,    updated: '2026-10-29',  },  {    id: 'notifier',    name: 'notifier',    owner: 'Jun Tanaka',    region: 'eu-north',    status: 'Live',    deploys: 97,    uptime: 99.97,    latency: 58,    cost: 310,    updated: '2026-10-27',  },  {    id: 'reports',    name: 'reports',    owner: 'Noor Haddad',    region: 'us-west',    status: 'Paused',    deploys: 41,    uptime: 99.8,    latency: 321,    cost: 190,    updated: '2026-10-22',  },  {    id: 'edge-cache',    name: 'edge-cache',    owner: 'Jun Tanaka',    region: 'global',    status: 'Live',    deploys: 318,    uptime: 100,    latency: 21,    cost: 1430,    updated: '2026-10-31',  },  {    id: 'auth',    name: 'auth',    owner: 'Mara Lindqvist',    region: 'eu-west',    status: 'Live',    deploys: 176,    uptime: 99.99,    latency: 66,    cost: 540,    updated: '2026-10-28',  },  {    id: 'ingest',    name: 'ingest',    owner: 'Lena Fischer',    region: 'us-east',    status: 'Live',    deploys: 264,    uptime: 99.93,    latency: 131,    cost: 2120,    updated: '2026-10-30',  },]const COMMANDS: Command[] = [  {    id: 'deploy',    label: 'Deploy storefront',    group: 'Projects',    icon: 'rocket',    shortcut: '⌘D',  },  {    id: 'logs',    label: 'Open logs',    group: 'Projects',    icon: 'external',  },  {    id: 'settings',    label: 'Open settings',    group: 'Go to',    icon: 'settings',    shortcut: '⌘,',  },  {    id: 'invite',    label: 'Invite a teammate',    group: 'Team',    icon: 'user',  },]const findProjects = async (  query: string,  signal: AbortSignal,): Promise<Command[]> => {  await new Promise((done) => {    setTimeout(done, 250)  })  if (signal.aborted || !query) return []  const matches = PROJECTS.filter((p) =>    p.name.includes(query.toLowerCase()),  )  return matches.map((p) => ({    id: p.id,    label: p.name,    group: 'Projects',    icon: 'folder',    hint: `${p.region} · ${p.status}`,  }))}

Prefixes for people and projects

Each of modes takes over the list when the search starts with its prefix. groupOrder fixes the order of the groups.

tsx
import { state } from '@kerf-js/core'import {  Button,  CommandPalette,  Stack,  Text} from '@kerf-js/veneer'import type {  Command,  CommandMode} from '@kerf-js/veneer'export function Modes() {  const open = state(false)  return (    <Stack      gap="related"      align="center"    >      <Button        emphasis="outline"        tone="neutral"        icon="search"        onClick={() => (open.value = true)}      >        Search everything      </Button>      <Text subtle>        Type @ for people or # for projects.      </Text>      <CommandPalette        open={open}        hotkey=""        commands={COMMANDS}        modes={MODES}        groupOrder={['Go to', 'Projects', 'Team']}      />    </Stack>  )}const PEOPLE = [  {    id: 'mara',    name: 'Mara Lindqvist',    email: 'mara@kerf.dev',    role: 'Owner',    team: 'Platform',    initials: 'ML',  },  {    id: 'jun',    name: 'Jun Tanaka',    email: 'jun@kerf.dev',    role: 'Admin',    team: 'Platform',    initials: 'JT',  },  {    id: 'priya',    name: 'Priya Raman',    email: 'priya@kerf.dev',    role: 'Member',    team: 'Web',    initials: 'PR',  },  {    id: 'tomas',    name: 'Tomás Oliveira',    email: 'tomas@kerf.dev',    role: 'Member',    team: 'Web',    initials: 'TO',  },  {    id: 'lena',    name: 'Lena Fischer',    email: 'lena@kerf.dev',    role: 'Member',    team: 'Data',    initials: 'LF',  },  {    id: 'noor',    name: 'Noor Haddad',    email: 'noor@kerf.dev',    role: 'Viewer',    team: 'Data',    initials: 'NH',  },  {    id: 'sam',    name: 'Sam Okafor',    email: 'sam@kerf.dev',    role: 'Member',    team: 'Mobile',    initials: 'SO',  },  {    id: 'ines',    name: 'Inés Vidal',    email: 'ines@kerf.dev',    role: 'Viewer',    team: 'Mobile',    initials: 'IV',  },]type Status =  | 'Live'  | 'Building'  | 'Failed'  | 'Paused'const PROJECTS: {  id: string  name: string  owner: string  region: string  status: Status  deploys: number  uptime: number  latency: number  cost: number  updated: string}[] = [  {    id: 'storefront',    name: 'storefront',    owner: 'Priya Raman',    region: 'eu-west',    status: 'Live',    deploys: 412,    uptime: 99.98,    latency: 84,    cost: 1240,    updated: '2026-10-31',  },  {    id: 'checkout',    name: 'checkout',    owner: 'Tomás Oliveira',    region: 'eu-west',    status: 'Live',    deploys: 288,    uptime: 99.95,    latency: 112,    cost: 980,    updated: '2026-10-31',  },  {    id: 'search-api',    name: 'search-api',    owner: 'Lena Fischer',    region: 'us-east',    status: 'Building',    deploys: 196,    uptime: 99.91,    latency: 146,    cost: 1610,    updated: '2026-10-30',  },  {    id: 'billing',    name: 'billing',    owner: 'Mara Lindqvist',    region: 'us-east',    status: 'Live',    deploys: 154,    uptime: 99.99,    latency: 73,    cost: 720,    updated: '2026-10-29',  },  {    id: 'mobile-bff',    name: 'mobile-bff',    owner: 'Sam Okafor',    region: 'ap-south',    status: 'Failed',    deploys: 233,    uptime: 99.42,    latency: 208,    cost: 860,    updated: '2026-10-29',  },  {    id: 'notifier',    name: 'notifier',    owner: 'Jun Tanaka',    region: 'eu-north',    status: 'Live',    deploys: 97,    uptime: 99.97,    latency: 58,    cost: 310,    updated: '2026-10-27',  },  {    id: 'reports',    name: 'reports',    owner: 'Noor Haddad',    region: 'us-west',    status: 'Paused',    deploys: 41,    uptime: 99.8,    latency: 321,    cost: 190,    updated: '2026-10-22',  },  {    id: 'edge-cache',    name: 'edge-cache',    owner: 'Jun Tanaka',    region: 'global',    status: 'Live',    deploys: 318,    uptime: 100,    latency: 21,    cost: 1430,    updated: '2026-10-31',  },  {    id: 'auth',    name: 'auth',    owner: 'Mara Lindqvist',    region: 'eu-west',    status: 'Live',    deploys: 176,    uptime: 99.99,    latency: 66,    cost: 540,    updated: '2026-10-28',  },  {    id: 'ingest',    name: 'ingest',    owner: 'Lena Fischer',    region: 'us-east',    status: 'Live',    deploys: 264,    uptime: 99.93,    latency: 131,    cost: 2120,    updated: '2026-10-30',  },]const COMMANDS: Command[] = [  {    id: 'deploy',    label: 'Deploy storefront',    group: 'Projects',    icon: 'rocket',    shortcut: '⌘D',  },  {    id: 'logs',    label: 'Open logs',    group: 'Projects',    icon: 'external',  },  {    id: 'settings',    label: 'Open settings',    group: 'Go to',    icon: 'settings',    shortcut: '⌘,',  },  {    id: 'invite',    label: 'Invite a teammate',    group: 'Team',    icon: 'user',  },]const MODES: CommandMode[] = [  {    prefix: '@',    label: 'People',    placeholder: 'Find a teammate',    commands: PEOPLE.map((p) => ({      id: p.id,      label: p.name,      group: 'People',      icon: 'user',      hint: p.team,    })),  },  {    prefix: '#',    label: 'Projects',    placeholder: 'Find a project',    commands: PROJECTS.map((p) => ({      id: p.id,      label: p.name,      group: 'Projects',      icon: 'folder',      hint: p.region,    })),  },]

When to use

Use it for

  • An app with more commands than fit on screen
  • People who prefer the keyboard

Reach for something else

  • Menu, when there are only a few actions

Accessibility

⌘K
Opens it from anywhere
↑↓
Moves through the results
Enter
Runs the highlighted command
Esc
Closes and returns focus

API Reference

PropTypeDefaultDescription
openboolean | State<boolean>—A state box that holds whether it is open.
defaultOpenbooleanfalseStarts open, when the component keeps its own open state.
commandsCommand[]—What can be found: label, group, icon, shortcut.
sourcesCommandSource[]—Searched as you type, after commands.
modesCommandMode[]—Prefixes that switch what is searched: > for commands, @ for people.
groupOrderstring[]—The order groups show in, whatever order rows arrive. Groups not named follow.
labelstring—The dialog's accessible name, and the input's label.
placeholderstring—The input's placeholder when no mode is on.
hotkeystring | false—The letter that opens it with ⌘ or Ctrl.
slashbooleanfalse/ opens it too, outside a field.
EventTypeDefaultDescription
onOpenChange((o: boolean) => void)—Called with true when it opens and false when it closes.
onRun((command: Command, reason: "keyboard" | "pointer") => void)—Called with the command that was picked.

Parts

Every element carries a stable name, so you can style it from your own CSS: [data-scope="palette"] [data-part="list"]

listshuttleemptysr-onlyfooter

RelatedMenuModal