Command palette search
A keyboard driven command palette that opens with Ctrl or Cmd K, filters a list as you type and runs the highlighted entry.
Tailwind CSSSearchcommand-palettesearchkeyboarddialog
Live preview
Code
<dialog id="palette" aria-label="Command palette"
class="mx-auto mt-24 w-full max-w-lg rounded-xl bg-white p-0 shadow-2xl backdrop:bg-slate-900/50">
<input id="palette-input" type="text" role="combobox" aria-expanded="true" aria-controls="palette-list"
aria-autocomplete="list" placeholder="Search docs..." autocomplete="off"
class="w-full border-b border-slate-200 px-4 py-3 text-slate-900 outline-none">
<ul id="palette-list" role="listbox" class="max-h-72 overflow-y-auto p-2 text-sm"></ul>
<p class="border-t border-slate-100 px-4 py-2 text-xs text-slate-400">Arrows to move, Enter to open, Esc to close</p>
</dialog>
const items = [
{ label: 'Getting started', href: '/docs/start' },
{ label: 'Authentication', href: '/docs/auth' },
{ label: 'Webhooks', href: '/docs/webhooks' },
{ label: 'API reference', href: '/docs/api' },
{ label: 'Troubleshooting', href: '/docs/help' },
];
const dialog = document.getElementById('palette');
const input = document.getElementById('palette-input');
const list = document.getElementById('palette-list');
let shown = items;
let index = 0;
function render() {
list.replaceChildren(...shown.map((item, i) => {
const li = document.createElement('li');
li.id = 'palette-opt-' + i;
li.setAttribute('role', 'option');
li.setAttribute('aria-selected', String(i === index));
li.className = 'cursor-pointer rounded-lg px-3 py-2 aria-selected:bg-indigo-600 aria-selected:text-white';
li.textContent = item.label;
li.addEventListener('click', () => (location.href = item.href));
return li;
}));
input.setAttribute('aria-activedescendant', shown.length ? 'palette-opt-' + index : '');
}
input.addEventListener('input', () => {
const q = input.value.trim().toLowerCase();
shown = items.filter((it) => it.label.toLowerCase().includes(q));
index = 0;
render();
});
input.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
index = (index + (e.key === 'ArrowDown' ? 1 : -1) + shown.length) % (shown.length || 1);
render();
} else if (e.key === 'Enter' && shown[index]) {
location.href = shown[index].href;
}
});
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
e.preventDefault(); // stop the browser's own shortcut
dialog.open ? dialog.close() : (dialog.showModal(), input.value = '', input.dispatchEvent(new Event('input')));
}
});
How it works
What it solves
Readers of large documentation sets want to jump to a page without touching the mouse. A palette gives a single shortcut, instant filtering and arrow key navigation using an array and a native dialog.
How it works
The panel is a dialog opened with showModal(), which gives a focus trap, an inert background and Escape handling for free. A global keydown listener watches for k with the Ctrl or Meta key and toggles it. The input handler lowercases the query, filters a plain array of {label, href} items and re-renders the list, setting role="option" and aria-selected on the active row. Arrow keys move the index with wrap-around, and Enter navigates to the active href. The input uses role="combobox" with aria-controls and aria-activedescendant so assistive tech follows the selection without moving focus off the field.
Adapting it
Replace the array with a fetched search index, or add a group field and render headings. For actions instead of links, store a function reference and call it on Enter.
Pitfalls
- Escape the label before inserting it as HTML; this version uses
textContent. - Prevent the default browser shortcut for Ctrl K or the address bar may steal it.
- Reset the index to zero whenever the filter changes.
How to use it
- Paste
palette.htmlnear the end of<body>in a Tailwind 4 page. - Load
palette.jswithdefer. - Edit the
itemsarray with your own labels and URLs. - Add a visible hint button elsewhere that calls
dialog.showModal()for touch users.
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.