Command palette

Pro

Search or jump: pages, threads, places and actions from one box (⌘K). Local groups filter as you type; remote groups are the service's results for the query, with a "Searching…" state while they're on the way and the failure said in words if the search failed. Closing it puts focus back where it was when it opened, even when a shortcut opened it. Choosing an item closes the palette, waits for focus to return, then runs it, so an action that moves focus (to a tab, a field) keeps it there.

Markdown

Preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/command-palette

Brings in: dialog, kbd, locale, use-shortcut, utils · npm: cmdk@^1.1.1, lucide-react@^1.67.0

Usage

TSX · The example above
"use client"

import * as React from "react"
import { Flag, GitBranch, MessageSquare, Plus, Settings, Table2 } from "lucide-react"
import { CommandPalette, CommandTrigger, matchEveryWord, type PaletteGroup } from "@/components/command-palette"

const recentThreads = [
  { id: "t1", label: "Add a region column to orders", detail: "Storefront · staging" },
  { id: "t2", label: "Why is checkout slow on Staging?", detail: "Storefront · staging" },
  { id: "t3", label: "Clean up stale feature flags", detail: "Storefront · Production" },
]

export default function Example() {
  const [open, setOpen] = React.useState(false)
  const [query, setQuery] = React.useState("")

  const groups: PaletteGroup[] = [
    {
      heading: "Actions",
      items: [
        { id: "new", label: "New thread", Icon: Plus, shortcut: "mod+shift+o", onSelect: () => {} },
        { id: "settings", label: "Open settings", Icon: Settings, onSelect: () => {} },
        { id: "deploy", label: "Promote Staging to Production", Icon: GitBranch, locked: "Only owners can promote to Production", onSelect: () => {} },
      ],
    },
    {
      heading: "Places",
      items: [
        { id: "flags", label: "Feature flags", Icon: Flag, keywords: ["toggles"], onSelect: () => {} },
        { id: "tables", label: "Tables", Icon: Table2, keywords: ["schema", "database"], onSelect: () => {} },
      ],
    },
    {
      // A real app fetches these from its search service as the query changes; here they're filtered locally.
      heading: "Threads",
      remote: true,
      items: query ? recentThreads.filter((t) => t.label.toLowerCase().includes(query.toLowerCase())).map((t) => ({ ...t, Icon: MessageSquare, onSelect: () => {} })) : [],
    },
  ]

  return (
    <div className="max-w-64">
      <CommandTrigger onClick={() => setOpen(true)} />
      {/* matchEveryWord: every word typed must start a word of the item, in any order ("flags feature"). */}
      <CommandPalette open={open} onOpenChange={setOpen} groups={groups} onQueryChange={setQuery} filter={matchEveryWord} />
    </div>
  )
}

<CommandPalette>

PropTypeDefaultDescription
openrequiredboolean—Whether the palette is showing.
onOpenChangerequired(open: boolean) => void—Called when the palette opens or closes, including after an item is chosen.
groupsrequiredPaletteGroup[]—The items, under their headings; local groups filter as the person types.
onQueryChange((query: string) => void) | undefined—Called as the query changes, to fetch remote groups.
searchingboolean | undefinedfalseRemote results are on the way; shows a spinner and "Searching…". Defaults to false.
searchErrorstring | undefined—Why the remote search failed, shown in place of "No results".
placeholderstring | undefined—Shown in the empty search box. Defaults to "Search or jump to…".
filter((value: string, search: string, keywords?: string[]) => number) | undefined—How local items match the query, passed to cmdk: `(value, search, keywords) => score`, 0 hiding the item. `keywords` holds the item's label, detail and keywords; `value` holds ids, so match on `keywords`. `matchEveryWord` is a strict "every word, any order" matcher. Defaults to cmdk's fuzzy match.
onCloseAutoFocus((event: Event) => void) | undefined—Called as the palette closes, just before focus returns to the element that had it when the palette opened. Call `event.preventDefault()` to put focus somewhere else yourself.

<CommandTrigger>

The button that opens the palette, for a sidebar or top bar.

PropTypeDefaultDescription
onClickrequired() => void—Called when the button is pressed, usually to open the palette.
labelstring | undefined—The button's text. Defaults to "Search or jump".
combostring | undefined"mod+k"The shortcut printed on the button, e.g. "mod+k" (the default). Printed, not bound.
classNamestring | undefined—Extra classes for the outer element.

Functions

matchEveryWord(_value: string, search: string, keywords?: string[])

A strict match for CommandPalette's filter: every word typed must start a word of the item's label, detail or keywords, in any order ("plan open" finds "Open the plan"). Case and accents are ignored. It reads cmdk's keywords only, never its value, which holds the group and item ids. Returns 1 for a match, 0 otherwise, so cmdk keeps the items in the order you gave them.

Strings

The fixed words, in English by default. Change any of them under commandPalette in <LocaleProvider strings={{ … }}>; the table is exported as commandPaletteStrings.

KeyEnglish
titleSearch or jump
descriptionType to find pages, threads, places and actions.
placeholderSearch or jump to…
searchSearch
resultsResults
searchingStatusSearching
searchingSearching…
nothingMatchesNothing matches “{query}”.
nothingToShowNothing to show.
searchFailedSearch failed: {error}
noResultsNo results.
movemove
openopen
closeclose
triggerSearch or jump

Types

PaletteItem
{ id: string label: string /** Shown after the label: a place, a date, a path. */ detail?: string | undefined Icon?: LucideIcon | undefined /** Extra words that should find this item. */ keywords?: string[] | undefined /** A combo that also does this, e.g. "mod+shift+o". Printed, not bound. */ shortcut?: string | undefined /** Why it can't be done now; the item stays listed but can't be chosen. */
PaletteGroup
{ heading: string items: PaletteItem[] /** * Results the service already matched against the query (search across * threads, Know, the audit log). They're shown as given, not filtered again. */ remote?: boolean | undefined }