PayloadCMSExtensions
Plugins

Command Menu (CMDK) Plugin

A powerful command menu plugin for Payload CMS that enhances navigation and accessibility within the admin panel with keyboard shortcuts.

Overview

The Payload CMDK Plugin adds a powerful command menu to your Payload CMS admin panel, enabling quick search and navigation through collections, globals, and custom actions using keyboard shortcuts.

Demo of opening command menu, searching for a collection, and navigating

Features

  • Quick Search: Instantly search across all collections and globals
  • Keyboard Shortcuts: Fully customizable keyboard shortcuts powered by react-hotkeys-hook
  • Collection Submenu: Search within collection documents by their title field
  • Custom Icons: Use any Lucide icon for collections and globals
  • Custom Items: Add custom actions and menu groups
  • i18n Support: Built-in English and Ukrainian translations, easily add your own
  • Cross-platform: Optimized shortcuts for both macOS and Windows/Linux

Installation

npm install @veiag/payload-cmdk

Quick Start

1. Add Plugin to Config

The plugin works out of the box with minimal configuration:

import { payloadCmdk } from '@veiag/payload-cmdk'
import { buildConfig } from 'payload'

export default buildConfig({
  // ... your existing config
  plugins: [
    payloadCmdk({
      // Plugin works without any options!
    }),
  ],
})

2. Start Your Server

The plugin will automatically:

  • Add a search button to the admin panel
  • Enable ⌘K (Mac) / Ctrl+K (Windows/Linux) keyboard shortcut
  • List all collections and globals in the command menu
  • Enable collection submenu search

Default Behavior

By default, the command menu can be opened with ⌘K on macOS or Ctrl+K on Windows/Linux. A search button will appear in the actions area of your admin panel.

Configuration

Plugin Options

Prop

Type

Full Configuration Example

import { payloadCmdk } from '@veiag/payload-cmdk'
import { buildConfig } from 'payload'

export default buildConfig({
  plugins: [
    payloadCmdk({
      // Keyboard shortcut to open the menu
      shortcut: ['meta+k', 'ctrl+k'],

      // Search button configuration
      searchButton: {
        position: 'actions', // 'actions' | 'nav'
      },

      // Backdrop blur effect
      blurBg: true,

      // Collection submenu configuration
      submenu: {
        enabled: true,
        shortcut: 'shift+enter',
        icons: {
          posts: 'FileText',
          users: 'User',
        },
      },

      // Custom icons for collections and globals
      icons: {
        collections: {
          posts: 'FileText',
          pages: 'File',
          media: 'Image',
          users: 'Users',
        },
        globals: {
          settings: 'Settings',
          navigation: 'Menu',
        },
      },

      // Collections/globals to ignore
      slugsToIgnore: ['payload-migrations', 'payload-preferences'],

      // Custom menu items
      customItems: [
        {
          type: 'group',
          title: 'Quick Actions',
          items: [
            {
              type: 'item',
              slug: 'view-site',
              label: 'View Site',
              icon: 'ExternalLink',
              action: {
                type: 'link',
                href: 'https://your-site.com',
              },
            },
          ],
        },
      ],
    }),
  ],
})

Keyboard Shortcuts

Global Shortcuts

ShortcutAction
⌘K / Ctrl+KOpen/close command menu
EscClose menu or go back in submenu
Navigate items
EnterSelect item or navigate to collection
Shift+EnterSearch within collection (default)

In Submenu

ShortcutAction
EscGo back to main menu
EnterOpen selected document

Custom Shortcuts

Customize the keyboard shortcut using react-hotkeys-hook syntax:

// Single shortcut
payloadCmdk({
  shortcut: 'ctrl+shift+k',
})

// Multiple shortcuts for cross-platform support
payloadCmdk({
  shortcut: ['meta+k', 'ctrl+k'],
})

// Alternative shortcuts
payloadCmdk({
  shortcut: ['meta+/', 'ctrl+/'],
})

Cross-platform Support

Use meta for macOS Command key and ctrl for Windows/Linux Control key. Provide both for optimal cross-platform experience.

Search Button

Position Options

The search button can be displayed in two locations:

Actions Position (Default)

payloadCmdk({
  searchButton: {
    position: 'actions',
  },
})

Navigation Position

payloadCmdk({
  searchButton: {
    position: 'nav',
  },
})

Hide Search Button

payloadCmdk({
  searchButton: false, // Keyboard shortcut still works
})

Actions button position:

Actions button

Navigation button position:

Navigation button

Collection Submenu

The submenu feature allows searching within collection documents directly from the command menu.

Prop

Type

Shortcut Behavior

Default (shift+enter):

  • Enter: Navigate to collection list
  • Shift+Enter: Open submenu to search within collection

Alternative (enter):

  • Enter: Open submenu to search within collection
  • Shift+Enter: Navigate to collection list

Example

payloadCmdk({
  submenu: {
    enabled: true,
    shortcut: 'enter',
    icons: {
      posts: 'FileText',
      products: 'ShoppingCart',
    },
  },
})

Searching within a collection submenu

Title Field Configuration

The submenu searches documents by their useAsTitle field. Configure this in your collection:

{
  slug: 'posts',
  admin: {
    useAsTitle: 'title', // Submenu will search by this field
  },
  fields: [
    {
      name: 'title',
      type: 'text',
      required: true,
    },
    // ... other fields
  ],
}

Important

Ensure your useAsTitle field returns a string value. The plugin currently doesn't support non-string title fields, which may cause rendering errors.

Custom Icons

Customize icons using Lucide icon names.

Default Icons

  • Collections: Files icon
  • Globals: Globe icon

Icon Configuration

payloadCmdk({
  icons: {
    collections: {
      posts: 'FileText',
      pages: 'File',
      media: 'Image',
      users: 'Users',
      categories: 'Folder',
      products: 'ShoppingCart',
      orders: 'Receipt',
    },
    globals: {
      settings: 'Settings',
      navigation: 'Menu',
      footer: 'Layout',
      seo: 'Search',
    },
  },
})

Browse all available icons at lucide.dev/icons.

Icons preview

Custom Menu Items

Add custom actions and external links to the command menu.

Custom Item Structure

interface CustomMenuItem {
  type: 'item'
  slug: string
  label: string | LocalizedString
  icon?: LucideIconName
  action: {
    type: 'link' | 'api'
    href: string
    method?: 'GET' | 'POST' | 'PUT' | 'DELETE' // For API actions
    body?: Record<string, any> // For API actions
  }
}

interface CustomMenuGroup {
  type: 'group'
  title: string | LocalizedString
  items: CustomMenuItem[]
}

Examples

Link Action

payloadCmdk({
  customItems: [
    {
      type: 'item',
      slug: 'view-site',
      label: 'View Site',
      icon: 'ExternalLink',
      action: {
        type: 'link',
        href: 'https://your-site.com',
      },
    },
    {
      type: 'item',
      slug: 'documentation',
      label: 'View Documentation',
      icon: 'BookOpen',
      action: {
        type: 'link',
        href: 'https://docs.your-site.com',
      },
    },
  ],
})

API Action

payloadCmdk({
  customItems: [
    {
      type: 'item',
      slug: 'clear-cache',
      label: 'Clear Cache',
      icon: 'Trash2',
      action: {
        type: 'api',
        method: 'POST',
        href: '/api/cache/clear',
      },
    },
    {
      type: 'item',
      slug: 'regenerate',
      label: 'Regenerate Cache',
      icon: 'RefreshCw',
      action: {
        type: 'api',
        method: 'POST',
        href: '/api/cache/regenerate',
        body: { force: true },
      },
    },
  ],
})

Grouped Items

payloadCmdk({
  customItems: [
    {
      type: 'group',
      title: 'Quick Actions',
      items: [
        {
          type: 'item',
          slug: 'view-site',
          label: 'View Site',
          icon: 'ExternalLink',
          action: {
            type: 'link',
            href: 'https://your-site.com',
          },
        },
        {
          type: 'item',
          slug: 'clear-cache',
          label: 'Clear Cache',
          icon: 'Trash2',
          action: {
            type: 'api',
            method: 'POST',
            href: '/api/cache/clear',
          },
        },
      ],
    },
  ],
})

With Localization

payloadCmdk({
  customItems: [
    {
      type: 'group',
      title: {
        en: 'Quick Actions',
        uk: 'Швидкі дії',
      },
      items: [
        {
          type: 'item',
          slug: 'view-site',
          label: {
            en: 'View Site',
            uk: 'Переглянути сайт',
          },
          icon: 'ExternalLink',
          action: {
            type: 'link',
            href: 'https://your-site.com',
          },
        },
      ],
    },
  ],
})

Ignoring Collections

Exclude specific collections or globals from the command menu.

Default Ignored Slugs

['payload-migrations', 'payload-preferences', 'payload-locked-documents']

Add to Ignore List

payloadCmdk({
  slugsToIgnore: ['internal-collection', 'test-data', 'drafts'],
})

Replace Default Ignore List

payloadCmdk({
  slugsToIgnore: {
    ignoreList: ['my-hidden-collection'],
    replaceDefaults: true, // Only ignore specified collections
  },
})

Internationalization

The plugin includes built-in translations for English and Ukrainian. Add support for additional languages using Payload's i18n configuration.

Available Translation Keys

All translation keys are under the cmdkPlugin namespace:

KeyDescriptionExample (EN)
searchMain search placeholder"Search collections, globals..."
searchShortShort search label"Search"
searchInSubmenu search placeholder"Search in {{label}}"
loadingLoading state"Loading..."
noResultsNo results state"No results found"
navigateFooter hint for navigation"to navigate"
searchInCollectionFooter hint for collection search"to search in collection"
openFooter hint for opening documents"to open"

Adding Custom Translations

import { buildConfig } from 'payload'

export default buildConfig({
  i18n: {
    supportedLanguages: {
      en,
      uk,
      de,
      fr,
    },
    translations: {
      de: {
        cmdkPlugin: {
          loading: 'Lädt...',
          navigate: 'zum Navigieren',
          noResults: 'Keine Ergebnisse gefunden',
          open: 'zum Öffnen',
          search: 'Sammlungen, Globals durchsuchen...',
          searchIn: 'Suchen in {{label}}',
          searchInCollection: 'in Sammlung suchen',
          searchShort: 'Suchen',
        },
      },
      fr: {
        cmdkPlugin: {
          loading: 'Chargement...',
          navigate: 'pour naviguer',
          noResults: 'Aucun résultat trouvé',
          open: 'pour ouvrir',
          search: 'Rechercher collections, globals...',
          searchIn: 'Rechercher dans {{label}}',
          searchInCollection: 'pour rechercher dans la collection',
          searchShort: 'Rechercher',
        },
      },
    },
  },
  plugins: [
    payloadCmdk({
      // Your config
    }),
  ],
})

Examples

Minimal Setup

export default buildConfig({
  plugins: [payloadCmdk()],
})

Keyboard Shortcuts Only

export default buildConfig({
  plugins: [
    payloadCmdk({
      shortcut: ['meta+/', 'ctrl+/'],
      searchButton: false, // Hide button, only use keyboard
    }),
  ],
})

Blog Setup

export default buildConfig({
  plugins: [
    payloadCmdk({
      icons: {
        collections: {
          posts: 'Newspaper',
          authors: 'UserCircle',
          categories: 'FolderTree',
          tags: 'Tag',
          comments: 'MessageCircle',
        },
        globals: {
          settings: 'Settings',
          navigation: 'Menu',
        },
      },
      submenu: {
        enabled: true,
        icons: {
          posts: 'FileText',
          authors: 'User',
        },
      },
    }),
  ],
})

E-commerce Setup

export default buildConfig({
  plugins: [
    payloadCmdk({
      icons: {
        collections: {
          products: 'ShoppingCart',
          orders: 'Receipt',
          customers: 'Users',
          categories: 'Folder',
          reviews: 'Star',
        },
        globals: {
          settings: 'Settings',
          shipping: 'Truck',
        },
      },
      customItems: [
        {
          type: 'group',
          title: 'Store Actions',
          items: [
            {
              type: 'item',
              slug: 'view-store',
              label: 'View Storefront',
              icon: 'Store',
              action: {
                type: 'link',
                href: 'https://your-store.com',
              },
            },
            {
              type: 'item',
              slug: 'sync-inventory',
              label: 'Sync Inventory',
              icon: 'RefreshCw',
              action: {
                type: 'api',
                method: 'POST',
                href: '/api/inventory/sync',
              },
            },
          ],
        },
      ],
    }),
  ],
})

Full Custom Theme

export default buildConfig({
  plugins: [
    payloadCmdk({
      shortcut: ['meta+k', 'ctrl+k'],
      searchButton: {
        position: 'nav',
      },
      blurBg: true,
      icons: {
        collections: {
          posts: 'Newspaper',
          pages: 'FileText',
          media: 'Image',
          categories: 'FolderTree',
          tags: 'Tag',
          users: 'UserCircle',
          comments: 'MessageCircle',
        },
        globals: {
          header: 'LayoutTemplate',
          footer: 'Layout',
          settings: 'Settings',
          navigation: 'Menu',
          seo: 'Search',
        },
      },
      submenu: {
        enabled: true,
        shortcut: 'shift+enter',
        icons: {
          posts: 'FileText',
          pages: 'File',
          media: 'Image',
        },
      },
      customItems: [
        {
          type: 'group',
          title: 'Quick Links',
          items: [
            {
              type: 'item',
              slug: 'view-site',
              label: 'View Live Site',
              icon: 'ExternalLink',
              action: {
                type: 'link',
                href: 'https://your-site.com',
              },
            },
            {
              type: 'item',
              slug: 'documentation',
              label: 'Documentation',
              icon: 'BookOpen',
              action: {
                type: 'link',
                href: 'https://docs.your-site.com',
              },
            },
          ],
        },
      ],
      slugsToIgnore: ['payload-migrations', 'test-data'],
    }),
  ],
})

Troubleshooting

Objects are not valid as a React child

Cause: Your admin.useAsTitle field is returning an object instead of a string.

Solution: Ensure the field specified in useAsTitle returns a string value:

{
  slug: 'posts',
  admin: {
    useAsTitle: 'title', // Must be a simple text field
  },
  fields: [
    {
      name: 'title',
      type: 'text', // Not an object or complex field
      required: true,
    },
  ],
}

Command menu not opening

Solutions:

  1. Verify keyboard shortcut isn't conflicting with browser/OS shortcuts
  2. Check browser console for JavaScript errors
  3. Ensure the plugin is properly loaded in your config

Icons not displaying

Solutions:

  1. Verify icon names match exactly with Lucide icons
  2. Icon names are case-sensitive: use FileText, not filetext
  3. Check browser console for errors

Search button not visible

Solutions:

  1. Check searchButton configuration isn't set to false
  2. Verify position value is either 'actions' or 'nav'
  3. Clear browser cache and reload

License

MIT