Skip to content
weftGitHub

CommandPalette

Find any action by typing its name.

SourcePackage @weft-js/warp

Preview

tsx
import { state } from '@weft-js/core'import { Button, CommandPalette } from '@weft-js/warp'import type { Command } from '@weft-js/warp'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 @weft-js/warp

Usage

tsx
import { CommandPalette } from '@weft-js/warp'

Running a command

onRun gets the command that was picked.

tsx
import { state } from '@weft-js/core'import { Button, CommandPalette, Stack, Text } from '@weft-js/warp'import type { Command } from '@weft-js/warp'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 '@weft-js/core'import { Button, CommandPalette, Stack, Text } from '@weft-js/warp'import type { Command } from '@weft-js/warp'export function Nested() {  const open = state(false)  const region = state('eu-west')  const commands: Command[] = [    { id: 'region', label: 'Move storefront to…', 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 '@weft-js/core'import { Button, CommandPalette, Stack, Text } from '@weft-js/warp'import type { Command } from '@weft-js/warp'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 []  return PROJECTS.filter(p => p.name.includes(query.toLowerCase())).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 '@weft-js/core'import { Button, CommandPalette, Stack, Text } from '@weft-js/warp'import type { Command, CommandMode } from '@weft-js/warp'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@weft.dev', role: 'Owner', team: 'Platform', initials: 'ML' },  { id: 'jun', name: 'Jun Tanaka', email: 'jun@weft.dev', role: 'Admin', team: 'Platform', initials: 'JT' },  { id: 'priya', name: 'Priya Raman', email: 'priya@weft.dev', role: 'Member', team: 'Web', initials: 'PR' },  { id: 'tomas', name: 'Tomás Oliveira', email: 'tomas@weft.dev', role: 'Member', team: 'Web', initials: 'TO' },  { id: 'lena', name: 'Lena Fischer', email: 'lena@weft.dev', role: 'Member', team: 'Data', initials: 'LF' },  { id: 'noor', name: 'Noor Haddad', email: 'noor@weft.dev', role: 'Viewer', team: 'Data', initials: 'NH' },  { id: 'sam', name: 'Sam Okafor', email: 'sam@weft.dev', role: 'Member', team: 'Mobile', initials: 'SO' },  { id: 'ines', name: 'Inés Vidal', email: 'ines@weft.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'Command palette'The dialog's accessible name, and the input's label.
placeholderstring'Search or run a command…'The input's placeholder when no mode is on.
hotkeystring | false'k'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="input-row"]

input-rowsweep-basesweepsr-onlyinputmodelistshuttleemptyfooter

RelatedMenuModal