⌘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:
| Key | Action |
|---|---|
| Type | Filter — substring match across title + sub |
| ↑ / ↓ | Move active selection |
| Enter | Activate — navigate to href or run action() |
| Esc | Close |
| Click backdrop | Close |
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 bysectionand 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 7 —
fa-solid fa-*orfa-regular fa-*. Don’t reintroducefas fa-*(FA 5) orzmdi-*(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
- Theme switcher — the other runtime UI feature
- Interactive components — Inbox / Kanban / Data Table
- Architecture — how
main-vanilla.jsfits into the script load order