← Typing

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

  1. Paste the snippet just before </head>.
  2. Add data-command-palette-typing to a container with one element per command, and list the searches to type in data-query.

Options

Add any of these attributes to the same element.

data-querySearches to type, separated by |. Default the first word of the first command.
data-keyOn a command: a shortcut shown at the end of its row, like ⌘D.
data-iconOn a command: a short icon or emoji before its name.
data-placeholderText in the empty search box. Default “Type a command…”.
data-speedTyping time per character. Default 110 ms.
data-loopChoose the result, close, and type the next search.
data-holdHow 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.

1 · Snippet
<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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }

    // 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>
2 · Your HTML
<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>