C CoolAdmin v3.3.0

⌘K Command Palette

Press ⌘K or Ctrl+K to open a fuzzy-search modal over every page plus a couple of demo actions. Built-in 30-entry index, keyboard navigation, no external library. The topbar search field also opens the palette on focus.

Last updated May 22, 2026

The command palette is a global keyboard shortcut — ⌘K on macOS, Ctrl+K on Windows/Linux — that opens a fuzzy-search modal over every page in CoolAdmin plus a couple of demo actions.

It’s also wired into the topbar search field: focusing or clicking the search input opens the palette instead of typing inline. That makes the search box a discoverable affordance for users who don’t try keyboard shortcuts.

The implementation lives in js/main-vanilla.js under initCommandPalette(). No external fuzzy-search library — a simple substring match scores title + sub against the query.

What’s in it

30 commands across three sections:

  • Pages (18 entries) — Dashboard, Sales pipeline, Marketing analytics, Projects, Charts, Tables, Forms, Calendar, Maps, Inbox, Kanban board, Profile & settings, Pricing, Invoice, Notifications, Documentation, Setup wizard, Data table
  • Components (10 entries) — Buttons, Cards, Modals, Alerts, Badges, Tabs, Switches, Progress bars, Typography, Font Awesome icon set
  • Actions (2 entries) — Show success toast, Show error toast (demo wrappers)

Every entry has:

{
  section: 'Pages',                        // group header
  title:   'Sales pipeline',               // primary line
  sub:     'Index 2',                      // sub-line (deemphasized)
  href:    'index2.html',                  // navigates here on Enter
  icon:    'fa-handshake'                  // Font Awesome 7 class
}

Action entries swap href for an action function:

{
  section: 'Actions',
  title:   'Show success toast',
  sub:     'Demo a notification',
  action:  () => window.toast.success('Saved successfully'),
  icon:    'fa-circle-check'
}

The fuzzy matcher

Minimal — just title + sub lowercased and tested with includes(query):

function render(query) {
  const q = (query || '').trim().toLowerCase();
  const filtered = q
    ? COMMANDS.filter(c => (c.title + ' ' + (c.sub || '')).toLowerCase().includes(q))
    : COMMANDS;
  // …render grouped by `section`…
}

It’s substring matching, not subsequence-fuzzy like ⌘K palettes in some other tools. Typing tab matches “Tables” and “Tabs” (both contain tab), but not “Table” + “data” out of order. The 30-entry index is small enough that the simpler matcher reads cleanly and matches what users type 99% of the time.

If you wanted real fuzzy matching, swap the filter line for fzf-for-js or implement a subsequence scorer — see the Gentelella palette for an example.

Open / close

The global keybinding is installed once when initCommandPalette() runs (from main-vanilla.js):

document.addEventListener('keydown', (e) => {
  if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
    e.preventDefault();
    if (overlay && overlay.classList.contains('is-open')) close();
    else open();
  }
});

open() lazily builds the overlay DOM on first call, then reuses the same node across opens. Closing just toggles a class — the DOM stays attached.

A second entry point: the topbar search field. Focusing or clicking it opens the palette instead of letting the user type:

const topbarSearch = document.querySelector('.form-header .au-input');
if (topbarSearch) {
  topbarSearch.addEventListener('focus', (e) => {
    e.target.blur();
    open();
  });
  topbarSearch.addEventListener('mousedown', (e) => {
    e.preventDefault();
    open();
  });
}

The blur() + preventDefault() pair is what makes the search field act as an opener without ever taking focus — users land in the palette’s input instead.

The exported global window.cmdk = { open, close } lets other scripts open or close the palette programmatically.

Keyboard inside the palette

Once open:

KeyAction
TypeFilter — substring match across title + sub
↑ / ↓Move active selection
EnterActivate — navigate to href or run action()
EscClose
Click backdropClose

The active row gets is-active class. Click any row to activate without keyboard.

Adding a new command

Edit the COMMANDS array in initCommandPalette() (around line 1011 of js/main-vanilla.js):

const COMMANDS = [
  // …existing entries…

  // New page entry
  { section: 'Pages', title: 'Reports',   sub: 'Q2 summary', href: 'reports.html', icon: 'fa-chart-line' },

  // New action entry
  { section: 'Actions', title: 'Refresh data', sub: 'Reload all widgets',
    action: () => { window.location.reload(); }, icon: 'fa-arrows-rotate' },
];

Two requirements:

  • Section header must already exist'Pages', 'Components', 'Actions'. The renderer groups by section and emits one <li class="cmdk-section"> per unique section. Adding a brand-new section name (e.g. 'Reports') creates a new group automatically; otherwise the entry merges into the existing group.
  • Icon must be Font Awesome 7fa-solid fa-* or fa-regular fa-*. Don’t reintroduce fas fa-* (FA 5) or zmdi-* (Material Iconic, removed in v3.0).

After editing, rebuild via the Pug pipeline (npm run build) — but actually the palette is pure JS so a hard browser refresh is enough for dev. The npm run build rebuilds the static HTML; the palette JS is already in js/main-vanilla.js.

Why the topbar search opens the palette

Two reasons:

  • Most admin templates have a “Search” box in the topbar that does nothing. Wiring it to the palette makes the affordance functional without a separate search infrastructure.
  • Users who don’t try ⌘K still discover the palette. Click “Search…”, get a fuzzy-matching modal — same UX, no learning curve.

The downside: there’s no separate “global search across documents” feature. If your app needs that (search across emails, projects, customers, etc.), you’d build it as a separate page or a backend-driven endpoint — the palette is intentionally limited to navigation + demo actions.

See also