mdxui

CommandPalette

Keyboard-driven command palette widget for quick actions

CommandPalette

The CommandPalette widget provides a keyboard-driven interface for executing commands, navigating, and performing actions. It's typically triggered with Cmd+K and supports fuzzy search, keyboard shortcuts, and recent command history.

Type Signature

interface CommandPaletteProps {
  /** Available commands */
  commands: Array<{
    id: string
    name: string
    description?: string
    icon?: string
    shortcut?: string
    onExecute?: () => void
    category?: string
  }>
  /** Search query */
  query?: string
  /** Query change handler */
  onQueryChange?: (query: string) => void
  /** Recent commands */
  recentCommands?: string[]
  /** Show keyboard shortcuts */
  showShortcuts?: boolean
  /** Loading state */
  loading?: boolean
  /** Placeholder text */
  placeholder?: string
}

Usage

<CommandPalette
  commands={[
    {
      id: 'new-key',
      name: 'Create New API Key',
      description: 'Generate a new API key',
      icon: 'Plus',
      category: 'API Keys',
      shortcut: 'Cmd+N',
      onExecute: () => createApiKey(),
    },
    {
      id: 'settings',
      name: 'Open Settings',
      description: 'Configure your account',
      icon: 'Settings',
      category: 'Navigation',
      shortcut: 'Cmd+,',
      onExecute: () => navigate('/settings'),
    },
  ]}
  placeholder="Type a command or search..."
  showShortcuts={true}
/>

Props

commands

Array of available commands. Each command has a name, optional description, icon, keyboard shortcut, and execution handler.

<CommandPalette
  commands={[
    // Navigation commands
    {
      id: 'nav-dashboard',
      name: 'Go to Dashboard',
      category: 'Navigation',
      icon: 'LayoutDashboard',
      shortcut: 'G D',
      onExecute: () => navigate('/'),
    },
    {
      id: 'nav-keys',
      name: 'Go to API Keys',
      category: 'Navigation',
      icon: 'Key',
      shortcut: 'G K',
      onExecute: () => navigate('/keys'),
    },
    // Action commands
    {
      id: 'create-key',
      name: 'Create New API Key',
      category: 'Actions',
      icon: 'Plus',
      shortcut: 'Cmd+N',
      onExecute: () => createApiKey(),
    },
    {
      id: 'export-data',
      name: 'Export Data',
      category: 'Actions',
      icon: 'Download',
      onExecute: () => exportData(),
    },
  ]}
/>

query

Current search query value.

<CommandPalette
  query={query}
  onQueryChange={setQuery}
  commands={commands}
/>

onQueryChange

Callback when the search query changes.

<CommandPalette
  onQueryChange={(query) => {
    setQuery(query)
    // Filter commands based on query
    filterCommands(query)
  }}
/>

recentCommands

Array of recently executed command IDs to show when query is empty.

<CommandPalette
  commands={commands}
  recentCommands={['nav-dashboard', 'create-key', 'settings']}
/>

showShortcuts

Display keyboard shortcuts next to commands. Defaults to true.

<CommandPalette
  commands={commands}
  showShortcuts={true}
/>

placeholder

Placeholder text in the search input.

<CommandPalette placeholder="What do you want to do?" />

Widget Context Usage

CommandPalette is typically used as a modal overlay:

Full-screen command palette:

function App() {
  const [paletteOpen, setPaletteOpen] = useState(false)

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
        e.preventDefault()
        setPaletteOpen(true)
      }
    }

    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [])

  return (
    <>
      <Shell>
        <DashboardView />
      </Shell>

      <Dialog open={paletteOpen} onClose={() => setPaletteOpen(false)}>
        <CommandPalette
          commands={commands}
          onExecute={(command) => {
            command.onExecute?.()
            setPaletteOpen(false)
          }}
        />
      </Dialog>
    </>
  )
}

Quick command menu:

<Popup
  trigger={<Button size="icon"><Command /></Button>}
  position="bottom"
>
  <CommandPalette
    commands={quickCommands}
    placeholder="Quick action..."
  />
</Popup>

Command Categories

Organize commands into categories:

<CommandPalette
  commands={[
    // Navigation
    {
      id: 'nav-dashboard',
      name: 'Dashboard',
      category: 'Navigation',
      onExecute: () => navigate('/'),
    },
    {
      id: 'nav-keys',
      name: 'API Keys',
      category: 'Navigation',
      onExecute: () => navigate('/keys'),
    },
    // Create
    {
      id: 'create-key',
      name: 'New API Key',
      category: 'Create',
      onExecute: () => createKey(),
    },
    {
      id: 'create-webhook',
      name: 'New Webhook',
      category: 'Create',
      onExecute: () => createWebhook(),
    },
    // Settings
    {
      id: 'settings-account',
      name: 'Account Settings',
      category: 'Settings',
      onExecute: () => navigate('/settings/account'),
    },
    {
      id: 'settings-team',
      name: 'Team Settings',
      category: 'Settings',
      onExecute: () => navigate('/settings/team'),
    },
  ]}
/>

Common Patterns

Quick navigation to pages:

const navigationCommands = [
  {
    id: 'nav-dashboard',
    name: 'Dashboard',
    icon: 'LayoutDashboard',
    shortcut: 'G D',
    category: 'Go to',
    onExecute: () => navigate('/'),
  },
  {
    id: 'nav-keys',
    name: 'API Keys',
    icon: 'Key',
    shortcut: 'G K',
    category: 'Go to',
    onExecute: () => navigate('/keys'),
  },
  {
    id: 'nav-webhooks',
    name: 'Webhooks',
    icon: 'Webhook',
    shortcut: 'G W',
    category: 'Go to',
    onExecute: () => navigate('/webhooks'),
  },
  {
    id: 'nav-usage',
    name: 'Usage',
    icon: 'BarChart',
    shortcut: 'G U',
    category: 'Go to',
    onExecute: () => navigate('/usage'),
  },
  {
    id: 'nav-settings',
    name: 'Settings',
    icon: 'Settings',
    shortcut: 'G S',
    category: 'Go to',
    onExecute: () => navigate('/settings'),
  },
]

Action Commands

Quick actions and operations:

const actionCommands = [
  {
    id: 'new-key',
    name: 'Create API Key',
    description: 'Generate a new API key',
    icon: 'Plus',
    shortcut: 'Cmd+N',
    category: 'Actions',
    onExecute: () => createApiKey(),
  },
  {
    id: 'new-webhook',
    name: 'Create Webhook',
    description: 'Set up a new webhook endpoint',
    icon: 'Plus',
    category: 'Actions',
    onExecute: () => createWebhook(),
  },
  {
    id: 'export',
    name: 'Export Data',
    description: 'Download your data',
    icon: 'Download',
    category: 'Actions',
    onExecute: () => exportData(),
  },
  {
    id: 'invite',
    name: 'Invite Team Member',
    description: 'Send an invitation',
    icon: 'UserPlus',
    shortcut: 'Cmd+I',
    category: 'Actions',
    onExecute: () => inviteUser(),
  },
]

Theme Commands

Toggle theme and appearance:

const themeCommands = [
  {
    id: 'theme-light',
    name: 'Light Theme',
    icon: 'Sun',
    category: 'Theme',
    onExecute: () => setTheme('light'),
  },
  {
    id: 'theme-dark',
    name: 'Dark Theme',
    icon: 'Moon',
    category: 'Theme',
    onExecute: () => setTheme('dark'),
  },
  {
    id: 'theme-system',
    name: 'System Theme',
    icon: 'Monitor',
    category: 'Theme',
    onExecute: () => setTheme('system'),
  },
]

Help Commands

Access help and documentation:

const helpCommands = [
  {
    id: 'help-docs',
    name: 'Documentation',
    description: 'View API documentation',
    icon: 'Book',
    category: 'Help',
    onExecute: () => window.open('/docs', '_blank'),
  },
  {
    id: 'help-support',
    name: 'Contact Support',
    description: 'Get help from our team',
    icon: 'MessageSquare',
    category: 'Help',
    onExecute: () => openSupport(),
  },
  {
    id: 'help-changelog',
    name: 'What\'s New',
    description: 'See recent updates',
    icon: 'Sparkles',
    category: 'Help',
    onExecute: () => showChangelog(),
  },
]

Keyboard Shortcuts

Register global keyboard shortcuts:

function CommandPaletteWithShortcuts() {
  const [open, setOpen] = useState(false)

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      // Cmd+K to open palette
      if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
        e.preventDefault()
        setOpen(true)
        return
      }

      // Global shortcuts (when palette is closed)
      if (!open) {
        // G + D = Dashboard
        if (e.key === 'g') {
          e.preventDefault()
          const next = (ne: KeyboardEvent) => {
            if (ne.key === 'd') navigate('/')
            if (ne.key === 'k') navigate('/keys')
            if (ne.key === 's') navigate('/settings')
            window.removeEventListener('keydown', next)
          }
          window.addEventListener('keydown', next)
        }

        // Cmd+N = New API Key
        if ((e.metaKey || e.ctrlKey) && e.key === 'n') {
          e.preventDefault()
          createApiKey()
        }
      }
    }

    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [open])

  return (
    <CommandPalette
      open={open}
      onClose={() => setOpen(false)}
      commands={commands}
    />
  )
}

Recent Commands

Track and display recently used commands:

function CommandPaletteWithRecent() {
  const [recentCommands, setRecentCommands] = useState<string[]>([])

  const addRecentCommand = (commandId: string) => {
    setRecentCommands((prev) => {
      const filtered = prev.filter((id) => id !== commandId)
      return [commandId, ...filtered].slice(0, 5)
    })
  }

  return (
    <CommandPalette
      commands={commands}
      recentCommands={recentCommands}
      onExecute={(command) => {
        command.onExecute?.()
        addRecentCommand(command.id)
      }}
    />
  )
}

Implement fuzzy command search:

function FuzzyCommandPalette() {
  const [query, setQuery] = useState('')

  const fuzzyMatch = (command: Command, query: string) => {
    const commandText = `${command.name} ${command.description || ''}`.toLowerCase()
    const searchText = query.toLowerCase()

    // Simple fuzzy matching
    let searchIndex = 0
    for (let i = 0; i < commandText.length; i++) {
      if (commandText[i] === searchText[searchIndex]) {
        searchIndex++
        if (searchIndex === searchText.length) return true
      }
    }
    return false
  }

  const filteredCommands = commands.filter((cmd) =>
    query.length === 0 ? true : fuzzyMatch(cmd, query)
  )

  return (
    <CommandPalette
      query={query}
      onQueryChange={setQuery}
      commands={filteredCommands}
    />
  )
}

Command Scoring

Rank commands by relevance:

const scoreCommand = (command: Command, query: string) => {
  const name = command.name.toLowerCase()
  const q = query.toLowerCase()

  // Exact match
  if (name === q) return 100

  // Starts with query
  if (name.startsWith(q)) return 80

  // Contains query
  if (name.includes(q)) return 60

  // Fuzzy match
  if (fuzzyMatch(command, query)) return 40

  return 0
}

const sortedCommands = commands
  .map((cmd) => ({
    command: cmd,
    score: scoreCommand(cmd, query),
  }))
  .filter((item) => item.score > 0)
  .sort((a, b) => b.score - a.score)
  .map((item) => item.command)

Dynamic Commands

Generate commands dynamically:

function DynamicCommandPalette() {
  const { user, projects } = useData()

  const commands = useMemo(() => {
    const staticCommands = [
      { id: 'nav-dashboard', name: 'Dashboard', ... },
      { id: 'new-key', name: 'Create API Key', ... },
    ]

    // Add project navigation commands
    const projectCommands = projects.map((project) => ({
      id: `project-${project.id}`,
      name: `Go to ${project.name}`,
      category: 'Projects',
      icon: 'Folder',
      onExecute: () => navigate(`/projects/${project.id}`),
    }))

    // Add user-specific commands
    const userCommands = user.isAdmin
      ? [
          {
            id: 'admin-panel',
            name: 'Admin Panel',
            category: 'Admin',
            icon: 'Shield',
            onExecute: () => navigate('/admin'),
          },
        ]
      : []

    return [...staticCommands, ...projectCommands, ...userCommands]
  }, [user, projects])

  return <CommandPalette commands={commands} />
}

Command Groups

Organize commands into collapsible groups:

<CommandPalette
  commands={commands}
  renderGroups={(commands) => {
    const grouped = commands.reduce((acc, cmd) => {
      const category = cmd.category || 'Other'
      if (!acc[category]) acc[category] = []
      acc[category].push(cmd)
      return acc
    }, {})

    return Object.entries(grouped).map(([category, cmds]) => (
      <div key={category}>
        <h3 className="text-sm font-semibold mb-2">{category}</h3>
        <CommandList commands={cmds} />
      </div>
    ))
  }}
/>

Empty States

Handle empty states:

<CommandPalette
  query={query}
  commands={filteredCommands}
  emptyState={
    query.length === 0 ? (
      <div className="text-center py-8">
        <p className="text-muted-foreground">
          Type to search commands...
        </p>
      </div>
    ) : (
      <div className="text-center py-8">
        <p className="text-muted-foreground">
          No commands found for "{query}"
        </p>
      </div>
    )
  }
/>

Combine command execution with search:

function UnifiedPalette() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])

  useEffect(() => {
    if (query.startsWith('>')) {
      // Command mode
      const commandQuery = query.slice(1)
      setResults(commands.filter((c) =>
        c.name.toLowerCase().includes(commandQuery.toLowerCase())
      ))
    } else {
      // Search mode
      performSearch(query).then(setResults)
    }
  }, [query])

  return (
    <CommandPalette
      query={query}
      onQueryChange={setQuery}
      results={results}
      placeholder={
        query.startsWith('>') ? 'Type a command...' : 'Search or type > for commands...'
      }
    />
  )
}

Accessibility

CommandPalette implements accessibility best practices:

  • ARIA labels - Descriptive labels for screen readers
  • Keyboard navigation - Full keyboard support
  • Focus management - Proper focus handling
  • Live regions - Result updates announced
<div role="dialog" aria-modal="true" aria-label="Command Palette">
  <input
    type="text"
    role="combobox"
    aria-expanded="true"
    aria-controls="command-list"
    aria-activedescendant="command-1"
  />
  <ul id="command-list" role="listbox">
    <li id="command-1" role="option" aria-selected="true">
      Create API Key
      <kbd>Cmd+N</kbd>
    </li>
  </ul>
</div>

Best Practices

Use Clear Command Names

Make commands easy to find:

// Good - clear, searchable names
{ name: 'Create API Key', ... }
{ name: 'Export Data', ... }
{ name: 'Invite Team Member', ... }

// Bad - vague names
{ name: 'New', ... }
{ name: 'Download', ... }
{ name: 'Add User', ... }

Add Descriptions

Help users understand what commands do:

{
  name: 'Create API Key',
  description: 'Generate a new API key for authentication',
  ...
}

Organize commands by category:

commands.sort((a, b) => {
  if (a.category !== b.category) {
    return a.category.localeCompare(b.category)
  }
  return a.name.localeCompare(b.name)
})

Show Keyboard Shortcuts

Display shortcuts for discoverability:

<CommandPalette
  commands={commands}
  showShortcuts={true}
/>

Track Command Usage

Learn which commands are popular:

const trackCommandUsage = (commandId: string) => {
  analytics.track('command_executed', {
    command_id: commandId,
    timestamp: new Date().toISOString(),
  })
}

On this page