Command Palette Typing
A command palette springs open, a search types itself in and the list filters with every keystroke, the matching letters picked out in colour. The top result is chosen, the palette closes, and the next search begins.
Deploy to production
Toggle dark mode
Open settings
Duplicate page
Invite teammates
How to use it
- Paste the snippet just before
</head>. - Add
data-command-palette-typingto a container with one element per command, and list the searches to type indata-query.
Options
Add any of these attributes to the same element.
data-query | Searches to type, separated by |. Default the first word of the first command. |
data-key | On a command: a shortcut shown at the end of its row, like ⌘D. |
data-icon | On a command: a short icon or emoji before its name. |
data-placeholder | Text in the empty search box. Default “Type a command…”. |
data-speed | Typing time per character. Default 110 ms. |
data-loop | Choose the result, close, and type the next search. |
data-hold | How long each filtered list stays before a result is chosen. Default 1100 ms. |
Matching is forgiving: dep finds “Deploy” directly and “Duplicate page” by its letters in order, as real palettes do.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Command Palette Typing: a command palette pops open, a search query types
// itself in and the list filters live, highlighting the matching letters.
// The top result is "chosen" and the palette closes, ready for the next query.
// Usage:
// <div data-command-palette-typing data-query="deploy|dark">
// <p data-key="⌘D">Deploy to production</p>
// <p>Toggle dark mode</p>
// <p>Invite teammates</p>
// </div>
// One element per command; data-key adds a shortcut hint, data-icon an icon
// (any short text or emoji).
// Optional: data-query (queries to type, separated by |), data-placeholder
// (default "Type a command…"), data-speed (ms per character, default 110),
// data-loop (keep cycling through the queries), data-hold (ms each result
// stays before it is chosen, default 1100).
var DEFAULTS = { placeholder: 'Type a command…', speed: 110, hold: 1100 };
var css =
'.cpt{text-align:left}' +
'.cpt-pal{background:#fff;color:#18181b;border:1px solid rgba(24,24,27,.1);border-radius:12px;overflow:hidden;' +
'box-shadow:0 24px 50px -12px rgba(40,10,70,.35),0 2px 6px rgba(40,10,70,.08);transform-origin:50% 0}' +
'.cpt-input{display:flex;align-items:center;gap:0.6em;padding:0.75em 0.9em;border-bottom:1px solid rgba(24,24,27,.08)}' +
'.cpt-input svg{width:1.05em;height:1.05em;flex:none;color:#a1a1aa}' +
'.cpt-field{flex:1;min-width:0;white-space:pre;overflow:hidden;position:relative}' +
'.cpt-ph{position:absolute;left:0;top:0;color:#a1a1aa;pointer-events:none}' +
'.cpt-caret{display:inline-block;width:2px;height:1.15em;vertical-align:-0.2em;background:var(--accent,#6a0dad);margin-left:1px;position:relative;z-index:1}' +
'.cpt-caret.cpt-blink{animation:cpt-blink 1s steps(1) infinite}@keyframes cpt-blink{50%{opacity:0}}' +
'.cpt-esc{flex:none;font:inherit;font-size:0.7em;padding:0.15em 0.5em;border-radius:5px;border:1px solid rgba(24,24,27,.14);color:#71717a}' +
'.cpt-list{padding:0.35em}' +
'.cpt-row{display:flex;align-items:center;gap:0.65em;padding:0 0.6em;border-radius:8px;max-height:2.5em;height:2.5em;overflow:hidden;' +
'transition:max-height .28s cubic-bezier(.3,.7,.3,1),opacity .2s,background-color .18s,transform .15s;font-size:0.92em}' +
'.cpt-row.cpt-hide{max-height:0;opacity:0}' +
'.cpt-row.cpt-sel{background:var(--accent-soft,#f3e8ff);box-shadow:inset 3px 0 0 var(--accent,#6a0dad)}' +
'.cpt-row.cpt-go{transform:scale(.97)}' +
'.cpt-ico{flex:none;width:1.5em;height:1.5em;display:grid;place-items:center;border-radius:6px;background:rgba(24,24,27,.06);font-size:0.9em}' +
'.cpt-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
'.cpt-label mark{background:none;color:var(--accent,#6a0dad);font-weight:700}' +
'.cpt-key{flex:none;font-size:0.8em;color:#71717a;letter-spacing:.05em}' +
'.cpt-empty{display:none;padding:0.8em;color:#a1a1aa;font-size:0.9em;text-align:center}' +
'.cpt-none .cpt-empty{display:block}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
var SEARCH = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true">' +
'<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>';
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }
function esc(s) { return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
// Which letters of label match the query: a run if it appears as written,
// otherwise the letters in order (fuzzy). Returns null for no match.
function match(label, q) {
var lo = label.toLowerCase(), i = lo.indexOf(q), hits = [];
if (!q) return [];
if (i >= 0) { for (var k = 0; k < q.length; k++) hits.push(i + k); return hits; }
var from = 0;
for (var j = 0; j < q.length; j++) {
if (q.charAt(j) === ' ') continue;
var at = lo.indexOf(q.charAt(j), from);
if (at < 0) return null;
hits.push(at);
from = at + 1;
}
return hits;
}
function setup(el) {
var d = el.dataset;
var items = Array.from(el.children).map(function (c) {
return { label: c.textContent.trim(), key: c.getAttribute('data-key') || '', icon: c.getAttribute('data-icon') || '' };
});
el.textContent = '';
el.classList.add('cpt');
var pal = document.createElement('div');
pal.className = 'cpt-pal';
pal.innerHTML = '<div class="cpt-input">' + SEARCH + '<span class="cpt-field"><span class="cpt-ph"></span><span class="cpt-q"></span>' +
'<span class="cpt-caret" aria-hidden="true"></span></span><span class="cpt-esc" aria-hidden="true">esc</span></div>' +
'<div class="cpt-list" role="listbox"></div><div class="cpt-empty">No results</div>';
pal.querySelector('.cpt-ph').textContent = d.placeholder || DEFAULTS.placeholder;
var list = pal.querySelector('.cpt-list');
items.forEach(function (it) {
var row = document.createElement('div');
row.className = 'cpt-row';
row.setAttribute('role', 'option');
row.innerHTML = '<span class="cpt-ico" aria-hidden="true"></span><span class="cpt-label"></span><span class="cpt-key"></span>';
row.firstChild.textContent = it.icon || '›';
row.children[1].textContent = it.label;
row.lastChild.textContent = it.key;
list.appendChild(row);
it.row = row;
});
el.appendChild(pal);
var queries = (d.query || (items[0] ? items[0].label.split(' ')[0] : '')).split('|');
return {
el: el, pal: pal, items: items, queries: queries, qi: 0,
q: pal.querySelector('.cpt-q'), ph: pal.querySelector('.cpt-ph'), caret: pal.querySelector('.cpt-caret'),
visible: false, wake: null
};
}
function filter(s, query) {
var q = query.toLowerCase(), first = null;
s.q.textContent = query;
s.ph.style.visibility = query ? 'hidden' : '';
s.items.forEach(function (it) {
var hits = match(it.label, q);
var lab = it.row.children[1];
it.row.classList.toggle('cpt-hide', hits === null);
it.row.classList.remove('cpt-sel', 'cpt-go');
it.row.setAttribute('aria-selected', 'false');
if (hits === null) return;
if (!first) first = it;
lab.innerHTML = it.label.split('').map(function (ch, i) {
return hits.indexOf(i) >= 0 ? '<mark>' + esc(ch) + '</mark>' : esc(ch);
}).join('');
});
if (first) { first.row.classList.add('cpt-sel'); first.row.setAttribute('aria-selected', 'true'); }
s.pal.classList.toggle('cpt-none', !first);
return first;
}
function whenVisible(s) {
return s.visible ? Promise.resolve() : new Promise(function (r) { s.wake = r; });
}
function play(s) {
var d = s.el.dataset;
var speed = +d.speed || DEFAULTS.speed;
var query = s.queries[s.qi % s.queries.length];
var chosen = null;
s.qi++;
filter(s, '');
s.caret.classList.add('cpt-blink');
anime.animate(s.pal, { opacity: [0, 1], scale: [0.94, 1], y: [-10, 0], duration: 520, ease: 'outBack(1.4)' });
var chain = sleep(650);
query.split('').forEach(function (ch, i) {
chain = chain.then(function () {
s.caret.classList.remove('cpt-blink');
chosen = filter(s, query.slice(0, i + 1));
return sleep(speed * (0.6 + Math.random() * 0.8));
});
});
chain.then(function () {
s.caret.classList.add('cpt-blink');
return sleep(+d.hold || DEFAULTS.hold);
}).then(function () {
if (d.loop === undefined || d.loop === 'false') return;
if (chosen) chosen.row.classList.add('cpt-go');
return sleep(180).then(function () {
if (chosen) chosen.row.classList.remove('cpt-go');
return new Promise(function (r) {
anime.animate(s.pal, { opacity: 0, scale: 0.97, y: -6, duration: 260, delay: 120, ease: 'in(2)', onComplete: r });
});
}).then(function () { return sleep(500); })
.then(function () { return whenVisible(s); })
.then(function () { play(s); });
});
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var s = e.target._cpt;
s.visible = e.isIntersecting;
if (s.visible && s.wake) { var w = s.wake; s.wake = null; w(); }
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-command-palette-typing]').forEach(function (el) {
if (el._cpt) return;
var s = (el._cpt = setup(el));
if (reduced()) { filter(s, s.queries[0]); return; }
// Keep the full list's height so the page doesn't jump as rows filter out.
var fs = parseFloat(getComputedStyle(el).fontSize) || 16;
if (el.offsetHeight) el.style.minHeight = (el.offsetHeight / fs) + 'em';
anime.utils.set(s.pal, { opacity: 0 });
io.observe(el);
whenVisible(s).then(function () { play(s); });
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-command-palette-typing data-query="deploy|dark" data-loop>
<p data-key="⌘D">Deploy to production</p>
<p data-key="⌘J">Toggle dark mode</p>
<p>Invite teammates</p>
</div>