Open demo

CSS code

HTML
<div><span class="lbl">Opens with ⌘K or the button</span>
<button class="ck-trigger" type="button" command="show-modal" commandfor="ck-dialog" aria-keyshortcuts="Meta+K Control+K"><svg class="ck-tico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg><span class="ck-ttext">Search or jump to…</span><kbd class="ck-kbd ck-tkbd" data-cmdk-key aria-hidden="true">⌘K</kbd></button>
<p class="ck-out" id="ck-out" aria-live="polite">Press ⌘K or Ctrl+K, or use the button.</p>
<dialog class="ck-dialog" id="ck-dialog" aria-label="Command palette" closedby="any">
<div class="ck-panel" data-cmdk>
  <form class="ck-head" role="search" aria-label="Commands">
    <svg class="ck-hico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
    <label class="sr-only" for="ck-q">Search commands</label>
    <input class="ck-input" id="ck-q" type="search" placeholder="Type a command or search…" autocomplete="off" spellcheck="false" autofocus
      role="combobox" aria-expanded="true" aria-controls="ck-list" aria-autocomplete="list">
    <button class="ck-esc" type="button" command="close" commandfor="ck-dialog" aria-label="Close">Esc</button>
  </form>
  <div class="ck-list" id="ck-list" role="listbox" aria-label="Commands">
    <div class="ck-group" role="group" aria-labelledby="ck-g1">
      <div class="ck-gh" id="ck-g1">Suggestions</div>
      <div class="ck-opt" role="option" id="ck-o1" aria-selected="false" data-label="Create new issue" data-keywords="add ticket bug task"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 5v14M5 12h14"/></svg></span><span class="ck-olabel">Create new issue</span><span class="ck-keys" aria-hidden="true"><kbd class="ck-kbd">C</kbd></span></div>
      <div class="ck-opt" role="option" id="ck-o2" aria-selected="false" data-label="Search issues" data-keywords="find filter"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg></span><span class="ck-olabel">Search issues</span><span class="ck-keys" aria-hidden="true"><kbd class="ck-kbd">/</kbd></span></div>
      <div class="ck-opt" role="option" id="ck-o3" aria-selected="false" data-label="Go to inbox" data-keywords="notifications mentions"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4 13h4l1.5 3h5L16 13h4"/><path d="M5.5 5h13L20 13v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-5Z"/></svg></span><span class="ck-olabel">Go to inbox</span><span class="ck-keys" aria-hidden="true"><kbd class="ck-kbd">G</kbd><kbd class="ck-kbd">I</kbd></span></div>
    </div>
    <div class="ck-group" role="group" aria-labelledby="ck-g2">
      <div class="ck-gh" id="ck-g2">Settings</div>
      <div class="ck-opt" role="option" id="ck-o4" aria-selected="false" data-label="Billing and plans" data-keywords="invoice payment subscription upgrade"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 10h18M7 15h3"/></svg></span><span class="ck-olabel">Billing and plans</span></div>
      <div class="ck-opt" role="option" id="ck-o5" aria-selected="false" data-label="Team members" data-keywords="invite people seats roles"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="9" cy="8" r="3.5"/><path d="M3 20a6 6 0 0 1 12 0M16 4.5a3.5 3.5 0 0 1 0 7M21 20a6 6 0 0 0-3.5-5.4"/></svg></span><span class="ck-olabel">Team members</span></div>
      <div class="ck-opt" role="option" id="ck-o6" aria-selected="false" data-label="API keys" data-keywords="tokens developer webhooks"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="8" cy="15" r="4"/><path d="m11 12 9-9M17 6l3 3M15 8l2 2"/></svg></span><span class="ck-olabel">API keys</span></div>
    </div>
    <div class="ck-group" role="group" aria-labelledby="ck-g3">
      <div class="ck-gh" id="ck-g3">Help</div>
      <div class="ck-opt" role="option" id="ck-o7" aria-selected="false" data-label="Keyboard shortcuts" data-keywords="hotkeys keys"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 10h.01M10 10h.01M14 10h.01M18 10h.01M7 14h10"/></svg></span><span class="ck-olabel">Keyboard shortcuts</span><span class="ck-keys" aria-hidden="true"><kbd class="ck-kbd">?</kbd></span></div>
      <div class="ck-opt" role="option" id="ck-o8" aria-selected="false" data-label="Contact support" data-keywords="help chat email"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M9.5 9.5a2.5 2.5 0 1 1 3.5 2.3c-.6.3-1 .9-1 1.6V14M12 17h.01"/></svg></span><span class="ck-olabel">Contact support</span></div>
    </div>
  </div>
  <p class="ck-empty">No commands match.</p>
  <div class="ck-foot" aria-hidden="true"><span><kbd class="ck-kbd">↑</kbd><kbd class="ck-kbd">↓</kbd> move</span><span><kbd class="ck-kbd">↵</kbd> run</span><span><kbd class="ck-kbd">esc</kbd> close</span></div>
  <p class="sr-only" role="status"></p>
</div>
</dialog>
</div>

<div><span class="lbl">Inline quick switcher</span>
<div class="demo-panel">
<div class="ck-panel" data-cmdk>
  <form class="ck-head" role="search" aria-label="Projects and people">
    <svg class="ck-hico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
    <label class="sr-only" for="ckp-q">Jump to a project or person</label>
    <input class="ck-input" id="ckp-q" type="search" placeholder="Jump to a project or person…" autocomplete="off" spellcheck="false"
      role="combobox" aria-expanded="true" aria-controls="ckp-list" aria-autocomplete="list">
  </form>
  <div class="ck-list" id="ckp-list" role="listbox" aria-label="Projects and people">
    <div class="ck-group" role="group" aria-labelledby="ckp-g1">
      <div class="ck-gh" id="ckp-g1">Projects</div>
      <div class="ck-opt" role="option" id="ckp-o1" aria-selected="false" data-label="Atlas redesign" data-keywords="website marketing"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg></span><span class="ck-olabel">Atlas redesign</span></div>
      <div class="ck-opt" role="option" id="ckp-o2" aria-selected="false" data-label="Billing v2" data-keywords="payments stripe"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg></span><span class="ck-olabel">Billing v2</span></div>
      <div class="ck-opt" role="option" id="ckp-o3" aria-selected="false" data-label="Mobile onboarding" data-keywords="ios android app"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg></span><span class="ck-olabel">Mobile onboarding</span></div>
    </div>
    <div class="ck-group" role="group" aria-labelledby="ckp-g2">
      <div class="ck-gh" id="ckp-g2">People</div>
      <div class="ck-opt" role="option" id="ckp-o4" aria-selected="false" data-label="Maya Reyes" data-keywords="design lead"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg></span><span class="ck-olabel">Maya Reyes</span></div>
      <div class="ck-opt" role="option" id="ckp-o5" aria-selected="false" data-label="Tomasz Nowak" data-keywords="backend engineer"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg></span><span class="ck-olabel">Tomasz Nowak</span></div>
    </div>
  </div>
  <p class="ck-empty">No projects or people match.</p>
  <p class="sr-only" role="status"></p>
</div>
</div></div>
CSS
/* Command palette: a <dialog> opened with invoker commands, a combobox that filters a grouped listbox,
   and :has() for empty groups and the no-results message. The same panel also works inline. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.ck-panel,.ck-trigger{--ck-ink:#111827;--ck-text:#374151;--ck-muted:#6b7280;--ck-line:#7c8196;--ck-accent:#7c3aed;--ck-soft:#f5f3ff}
.ck-panel{display:flex;flex-direction:column;max-height:inherit;overflow:hidden;color:var(--ck-ink);background:#fff;border:1px solid #e5e7eb;border-radius:16px;
  box-shadow:0 1px 2px rgba(17,24,39,.05),0 30px 70px -20px rgba(17,24,39,.35)}
.ck-panel *,.ck-trigger *{box-sizing:border-box}
.ck-head{position:relative;display:flex;flex:none;align-items:center;gap:10px;margin:0;padding:0 12px 0 16px;border-bottom:1px solid #e5e7eb}
.ck-hico{flex:none;width:20px;height:20px;color:var(--ck-muted)}
.ck-panel .ck-input{flex:1;min-width:0;height:56px;margin:0;padding:0;font:inherit;font-size:1.02rem;color:var(--ck-ink);background:transparent;border:0;border-radius:0;
  box-shadow:none;outline:none;-webkit-appearance:none;appearance:none}
.ck-panel .ck-input::placeholder{color:var(--ck-muted);opacity:1}
.ck-input::-webkit-search-cancel-button{display:none}
.ck-kbd,.ck-esc{display:inline-grid;place-items:center;min-width:22px;height:22px;padding:0 6px;font:600 .72rem/1 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  color:var(--ck-text);background:#fff;border:1px solid #d1d5db;border-bottom-width:2px;border-radius:6px}
.ck-esc{height:26px;padding:0 8px;cursor:pointer}
.ck-esc:hover{background:#f3f4f6}
.ck-esc:focus-visible,.ck-trigger:focus-visible{outline:2px solid var(--ck-accent);outline-offset:2px}
.ck-list{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:8px}
.ck-group{margin-bottom:4px}
.ck-group:last-child{margin-bottom:0}
.ck-gh{padding:8px 10px 6px;font-size:.7rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--ck-muted)}
.ck-opt{display:flex;align-items:center;gap:12px;height:44px;padding:0 10px;border-radius:10px;font-size:.93rem;color:var(--ck-text);cursor:pointer}
.ck-opt[hidden]{display:none}
.ck-opt:hover{background:#f9fafb}
.ck-opt[aria-selected="true"]{background:var(--ck-soft);color:var(--ck-ink)}
.ck-oico{display:grid;place-items:center;flex:none;width:28px;height:28px;border:1px solid #e5e7eb;border-radius:8px;background:#fff;color:var(--ck-muted)}
.ck-opt[aria-selected="true"] .ck-oico{border-color:#ddd6fe;color:var(--ck-accent)}
.ck-oico svg{width:16px;height:16px}
.ck-olabel{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ck-keys{display:flex;flex:none;gap:4px}
/* :has() does the bookkeeping: empty groups disappear and the empty message shows when nothing matches */
.ck-group:not(:has([role="option"]:not([hidden]))){display:none}
.ck-empty{display:none;margin:0;padding:28px 16px 32px;text-align:center;font-size:.92rem;color:var(--ck-muted)}
.ck-list:not(:has([role="option"]:not([hidden]))) + .ck-empty{display:block}
.ck-foot{display:flex;flex:none;flex-wrap:wrap;gap:4px 16px;padding:10px 16px;font-size:.78rem;color:var(--ck-muted);background:#f9fafb;border-top:1px solid #e5e7eb}
.ck-foot .ck-kbd{min-width:20px;height:20px;margin-right:2px}
/* the trigger that looks like a search field */
.ck-trigger{display:flex;align-items:center;gap:10px;width:100%;max-width:440px;height:48px;margin:0;padding:0 10px 0 14px;font:inherit;font-size:.95rem;text-align:left;
  color:var(--ck-muted);background:#fff;border:1px solid var(--ck-line);border-radius:12px;box-shadow:0 1px 2px rgba(17,24,39,.05);cursor:pointer;transition:border-color .15s,box-shadow .15s}
.ck-trigger:hover{border-color:var(--ck-ink)}
.ck-tico{flex:none;width:18px;height:18px}
.ck-ttext{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ck-tkbd{height:24px;padding:0 8px;font-size:.75rem}
.ck-out{margin:12px 0 0;font-size:.88rem;color:#374151}
/* <dialog> opened as a modal: @starting-style animates it in, allow-discrete keeps it visible while it animates out */
.ck-dialog{width:min(640px,calc(100vw - 32px));max-height:min(520px,calc(100dvh - 96px));margin:12vh auto auto;padding:0;overflow:visible;border:0;background:transparent;
  opacity:1;transform:none;transition:opacity .18s ease,transform .18s ease,overlay .18s allow-discrete,display .18s allow-discrete}
.ck-dialog:not([open]){opacity:0;transform:translateY(-8px) scale(.98)}
.ck-dialog::backdrop{background:rgba(17,24,39,.45);-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);
  transition:background-color .18s,overlay .18s allow-discrete,display .18s allow-discrete}
.ck-dialog:not([open])::backdrop{background:rgba(17,24,39,0)}
@starting-style{
  .ck-dialog[open]{opacity:0;transform:translateY(-8px) scale(.98)}
  .ck-dialog[open]::backdrop{background:rgba(17,24,39,0)}
}
@media (max-width:480px){.ck-foot{display:none}}
@media (prefers-reduced-motion:reduce){.ck-dialog,.ck-dialog::backdrop,.ck-trigger{transition:none}.ck-dialog:not([open]){transform:none}}
JavaScript
// Command palette. The dialog opens natively through command="show-modal" (invoker commands); this script adds
// the ⌘K / Ctrl+K shortcut, filtering, arrow-key movement and a fallback for browsers without invoker commands.
(() => {
  const dialog = document.getElementById('ck-dialog');

  document.querySelectorAll('[data-cmdk]').forEach(panel => {
    const input = panel.querySelector('[role="combobox"]');
    const opts = [...panel.querySelectorAll('[role="option"]')];
    const status = panel.querySelector('[role="status"]');
    const shown = () => opts.filter(o => !o.hidden);
    const current = () => opts.find(o => o.getAttribute('aria-selected') === 'true');

    function select(o) {
      opts.forEach(x => x.setAttribute('aria-selected', String(x === o)));
      if (o) { input.setAttribute('aria-activedescendant', o.id); o.scrollIntoView({ block: 'nearest' }); }
      else input.removeAttribute('aria-activedescendant');
    }
    function filter() {
      const q = input.value.trim().toLowerCase();
      opts.forEach(o => { o.hidden = q !== '' && !(o.dataset.label + ' ' + o.dataset.keywords).toLowerCase().includes(q); });
      const list = shown();
      select(list[0]);
      status.textContent = q ? list.length + (list.length === 1 ? ' result' : ' results') : '';
    }
    function run(o) {
      panel.dispatchEvent(new CustomEvent('cmdk:run', { bubbles: true, detail: o.dataset.label }));
      panel.closest('dialog')?.close();
    }

    input.addEventListener('input', filter);
    input.addEventListener('keydown', e => {
      const list = shown(), i = list.indexOf(current());
      if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
        e.preventDefault();
        if (list.length) select(list[(i + (e.key === 'ArrowDown' ? 1 : -1) + list.length) % list.length]);
      } else if (e.key === 'Enter') {
        e.preventDefault();
        if (i > -1) run(list[i]);
      }
    });
    panel.addEventListener('mousedown', e => { if (e.target.closest('[role="option"]')) e.preventDefault(); });
    panel.addEventListener('click', e => { const o = e.target.closest('[role="option"]'); if (o) run(o); });
    panel.closest('dialog')?.addEventListener('close', () => { input.value = ''; filter(); });
    filter();
  });

  if (!dialog) return;
  // ⌘K on Apple devices, Ctrl+K elsewhere
  if (!/Mac|iPhone|iPad/.test(navigator.platform)) document.querySelectorAll('[data-cmdk-key]').forEach(k => { k.textContent = 'Ctrl K'; });
  document.addEventListener('keydown', e => {
    if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); dialog.open ? dialog.close() : dialog.showModal(); }
  });
  // light dismiss where closedby="any" is not supported yet
  dialog.addEventListener('click', e => { if (e.target === dialog) dialog.close(); });
  // fallback for browsers without invoker commands (command / commandfor)
  if (!('command' in HTMLButtonElement.prototype)) {
    document.querySelectorAll('button[commandfor="ck-dialog"]').forEach(b => b.addEventListener('click', () => {
      b.getAttribute('command') === 'close' ? dialog.close() : dialog.showModal();
    }));
  }
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Opens with ⌘K or the button</span>
<button class="btn ck-trigger" type="button" command="show-modal" commandfor="ck-dialog" aria-keyshortcuts="Meta+K Control+K"><svg class="ck-tico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg><span class="ck-ttext">Search or jump to…</span><kbd class="ck-kbd ck-tkbd" data-cmdk-key aria-hidden="true">⌘K</kbd></button>
<p class="ck-out" id="ck-out" aria-live="polite">Press ⌘K or Ctrl+K, or use the button.</p>
<dialog class="ck-dialog" id="ck-dialog" aria-label="Command palette" closedby="any">
<div class="ck-panel" data-cmdk>
  <form class="ck-head" role="search" aria-label="Commands">
    <svg class="ck-hico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
    <label class="visually-hidden" for="ck-q">Search commands</label>
    <input class="form-control ck-input" id="ck-q" type="search" placeholder="Type a command or search…" autocomplete="off" spellcheck="false" autofocus
      role="combobox" aria-expanded="true" aria-controls="ck-list" aria-autocomplete="list">
    <button class="btn ck-esc" type="button" command="close" commandfor="ck-dialog" aria-label="Close">Esc</button>
  </form>
  <div class="ck-list" id="ck-list" role="listbox" aria-label="Commands">
    <div class="ck-group" role="group" aria-labelledby="ck-g1">
      <div class="ck-gh" id="ck-g1">Suggestions</div>
      <div class="ck-opt" role="option" id="ck-o1" aria-selected="false" data-label="Create new issue" data-keywords="add ticket bug task"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 5v14M5 12h14"/></svg></span><span class="ck-olabel">Create new issue</span><span class="ck-keys" aria-hidden="true"><kbd class="ck-kbd">C</kbd></span></div>
      <div class="ck-opt" role="option" id="ck-o2" aria-selected="false" data-label="Search issues" data-keywords="find filter"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg></span><span class="ck-olabel">Search issues</span><span class="ck-keys" aria-hidden="true"><kbd class="ck-kbd">/</kbd></span></div>
      <div class="ck-opt" role="option" id="ck-o3" aria-selected="false" data-label="Go to inbox" data-keywords="notifications mentions"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4 13h4l1.5 3h5L16 13h4"/><path d="M5.5 5h13L20 13v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-5Z"/></svg></span><span class="ck-olabel">Go to inbox</span><span class="ck-keys" aria-hidden="true"><kbd class="ck-kbd">G</kbd><kbd class="ck-kbd">I</kbd></span></div>
    </div>
    <div class="ck-group" role="group" aria-labelledby="ck-g2">
      <div class="ck-gh" id="ck-g2">Settings</div>
      <div class="ck-opt" role="option" id="ck-o4" aria-selected="false" data-label="Billing and plans" data-keywords="invoice payment subscription upgrade"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 10h18M7 15h3"/></svg></span><span class="ck-olabel">Billing and plans</span></div>
      <div class="ck-opt" role="option" id="ck-o5" aria-selected="false" data-label="Team members" data-keywords="invite people seats roles"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="9" cy="8" r="3.5"/><path d="M3 20a6 6 0 0 1 12 0M16 4.5a3.5 3.5 0 0 1 0 7M21 20a6 6 0 0 0-3.5-5.4"/></svg></span><span class="ck-olabel">Team members</span></div>
      <div class="ck-opt" role="option" id="ck-o6" aria-selected="false" data-label="API keys" data-keywords="tokens developer webhooks"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="8" cy="15" r="4"/><path d="m11 12 9-9M17 6l3 3M15 8l2 2"/></svg></span><span class="ck-olabel">API keys</span></div>
    </div>
    <div class="ck-group" role="group" aria-labelledby="ck-g3">
      <div class="ck-gh" id="ck-g3">Help</div>
      <div class="ck-opt" role="option" id="ck-o7" aria-selected="false" data-label="Keyboard shortcuts" data-keywords="hotkeys keys"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 10h.01M10 10h.01M14 10h.01M18 10h.01M7 14h10"/></svg></span><span class="ck-olabel">Keyboard shortcuts</span><span class="ck-keys" aria-hidden="true"><kbd class="ck-kbd">?</kbd></span></div>
      <div class="ck-opt" role="option" id="ck-o8" aria-selected="false" data-label="Contact support" data-keywords="help chat email"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M9.5 9.5a2.5 2.5 0 1 1 3.5 2.3c-.6.3-1 .9-1 1.6V14M12 17h.01"/></svg></span><span class="ck-olabel">Contact support</span></div>
    </div>
  </div>
  <p class="ck-empty">No commands match.</p>
  <div class="ck-foot" aria-hidden="true"><span><kbd class="ck-kbd">↑</kbd><kbd class="ck-kbd">↓</kbd> move</span><span><kbd class="ck-kbd">↵</kbd> run</span><span><kbd class="ck-kbd">esc</kbd> close</span></div>
  <p class="visually-hidden" role="status"></p>
</div>
</dialog>
</div>

<div><span class="lbl">Inline quick switcher</span>
<div class="demo-panel">
<div class="ck-panel" data-cmdk>
  <form class="ck-head" role="search" aria-label="Projects and people">
    <svg class="ck-hico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
    <label class="visually-hidden" for="ckp-q">Jump to a project or person</label>
    <input class="form-control ck-input" id="ckp-q" type="search" placeholder="Jump to a project or person…" autocomplete="off" spellcheck="false"
      role="combobox" aria-expanded="true" aria-controls="ckp-list" aria-autocomplete="list">
  </form>
  <div class="ck-list" id="ckp-list" role="listbox" aria-label="Projects and people">
    <div class="ck-group" role="group" aria-labelledby="ckp-g1">
      <div class="ck-gh" id="ckp-g1">Projects</div>
      <div class="ck-opt" role="option" id="ckp-o1" aria-selected="false" data-label="Atlas redesign" data-keywords="website marketing"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg></span><span class="ck-olabel">Atlas redesign</span></div>
      <div class="ck-opt" role="option" id="ckp-o2" aria-selected="false" data-label="Billing v2" data-keywords="payments stripe"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg></span><span class="ck-olabel">Billing v2</span></div>
      <div class="ck-opt" role="option" id="ckp-o3" aria-selected="false" data-label="Mobile onboarding" data-keywords="ios android app"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg></span><span class="ck-olabel">Mobile onboarding</span></div>
    </div>
    <div class="ck-group" role="group" aria-labelledby="ckp-g2">
      <div class="ck-gh" id="ckp-g2">People</div>
      <div class="ck-opt" role="option" id="ckp-o4" aria-selected="false" data-label="Maya Reyes" data-keywords="design lead"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg></span><span class="ck-olabel">Maya Reyes</span></div>
      <div class="ck-opt" role="option" id="ckp-o5" aria-selected="false" data-label="Tomasz Nowak" data-keywords="backend engineer"><span class="ck-oico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg></span><span class="ck-olabel">Tomasz Nowak</span></div>
    </div>
  </div>
  <p class="ck-empty">No projects or people match.</p>
  <p class="visually-hidden" role="status"></p>
</div>
</div></div>
CSS
/* Bootstrap has no command palette, so the panel is custom CSS on top of .form-control, .btn and Bootstrap's reboot.
   The <dialog> opens with invoker commands; :has() hides empty groups and shows the no-results message. */
.ck-panel,.ck-trigger{--ck-ink:#111827;--ck-text:#374151;--ck-muted:#6b7280;--ck-line:#7c8196;--ck-accent:#7c3aed;--ck-soft:#f5f3ff}
.ck-panel{display:flex;flex-direction:column;max-height:inherit;overflow:hidden;color:var(--ck-ink);background:#fff;border:1px solid #e5e7eb;border-radius:16px;
  box-shadow:0 1px 2px rgba(17,24,39,.05),0 30px 70px -20px rgba(17,24,39,.35)}
.ck-panel *,.ck-trigger *{box-sizing:border-box}
.ck-head{position:relative;display:flex;flex:none;align-items:center;gap:10px;margin:0;padding:0 12px 0 16px;border-bottom:1px solid #e5e7eb}
.ck-hico{flex:none;width:20px;height:20px;color:var(--ck-muted)}
.ck-panel .ck-input{flex:1;min-width:0;height:56px;margin:0;padding:0;font:inherit;font-size:1.02rem;color:var(--ck-ink);background:transparent;border:0;border-radius:0;
  box-shadow:none;outline:none;-webkit-appearance:none;appearance:none}
.ck-panel .ck-input::placeholder{color:var(--ck-muted);opacity:1}
.ck-input::-webkit-search-cancel-button{display:none}
.ck-kbd,.ck-esc{display:inline-grid;place-items:center;min-width:22px;height:22px;padding:0 6px;font:600 .72rem/1 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  color:var(--ck-text);background:#fff;border:1px solid #d1d5db;border-bottom-width:2px;border-radius:6px}
.ck-esc{height:26px;padding:0 8px;cursor:pointer}
.ck-esc:hover{background:#f3f4f6}
.ck-esc:focus-visible,.ck-trigger:focus-visible{outline:2px solid var(--ck-accent);outline-offset:2px}
.ck-list{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:8px}
.ck-group{margin-bottom:4px}
.ck-group:last-child{margin-bottom:0}
.ck-gh{padding:8px 10px 6px;font-size:.7rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--ck-muted)}
.ck-opt{display:flex;align-items:center;gap:12px;height:44px;padding:0 10px;border-radius:10px;font-size:.93rem;color:var(--ck-text);cursor:pointer}
.ck-opt[hidden]{display:none}
.ck-opt:hover{background:#f9fafb}
.ck-opt[aria-selected="true"]{background:var(--ck-soft);color:var(--ck-ink)}
.ck-oico{display:grid;place-items:center;flex:none;width:28px;height:28px;border:1px solid #e5e7eb;border-radius:8px;background:#fff;color:var(--ck-muted)}
.ck-opt[aria-selected="true"] .ck-oico{border-color:#ddd6fe;color:var(--ck-accent)}
.ck-oico svg{width:16px;height:16px}
.ck-olabel{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ck-keys{display:flex;flex:none;gap:4px}
/* :has() does the bookkeeping: empty groups disappear and the empty message shows when nothing matches */
.ck-group:not(:has([role="option"]:not([hidden]))){display:none}
.ck-empty{display:none;margin:0;padding:28px 16px 32px;text-align:center;font-size:.92rem;color:var(--ck-muted)}
.ck-list:not(:has([role="option"]:not([hidden]))) + .ck-empty{display:block}
.ck-foot{display:flex;flex:none;flex-wrap:wrap;gap:4px 16px;padding:10px 16px;font-size:.78rem;color:var(--ck-muted);background:#f9fafb;border-top:1px solid #e5e7eb}
.ck-foot .ck-kbd{min-width:20px;height:20px;margin-right:2px}
/* the trigger that looks like a search field */
.ck-trigger{display:flex;align-items:center;gap:10px;width:100%;max-width:440px;height:48px;margin:0;padding:0 10px 0 14px;font:inherit;font-size:.95rem;text-align:left;
  color:var(--ck-muted);background:#fff;border:1px solid var(--ck-line);border-radius:12px;box-shadow:0 1px 2px rgba(17,24,39,.05);cursor:pointer;transition:border-color .15s,box-shadow .15s}
.ck-trigger:hover{border-color:var(--ck-ink)}
.ck-tico{flex:none;width:18px;height:18px}
.ck-ttext{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ck-tkbd{height:24px;padding:0 8px;font-size:.75rem}
.ck-out{margin:12px 0 0;font-size:.88rem;color:#374151}
/* <dialog> opened as a modal: @starting-style animates it in, allow-discrete keeps it visible while it animates out */
.ck-dialog{width:min(640px,calc(100vw - 32px));max-height:min(520px,calc(100dvh - 96px));margin:12vh auto auto;padding:0;overflow:visible;border:0;background:transparent;
  opacity:1;transform:none;transition:opacity .18s ease,transform .18s ease,overlay .18s allow-discrete,display .18s allow-discrete}
.ck-dialog:not([open]){opacity:0;transform:translateY(-8px) scale(.98)}
.ck-dialog::backdrop{background:rgba(17,24,39,.45);-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);
  transition:background-color .18s,overlay .18s allow-discrete,display .18s allow-discrete}
.ck-dialog:not([open])::backdrop{background:rgba(17,24,39,0)}
@starting-style{
  .ck-dialog[open]{opacity:0;transform:translateY(-8px) scale(.98)}
  .ck-dialog[open]::backdrop{background:rgba(17,24,39,0)}
}
@media (max-width:480px){.ck-foot{display:none}}
@media (prefers-reduced-motion:reduce){.ck-dialog,.ck-dialog::backdrop,.ck-trigger{transition:none}.ck-dialog:not([open]){transform:none}}
.ck-panel .form-control:focus{background:transparent;box-shadow:none}
.btn.ck-esc{--bs-btn-padding-x:8px;--bs-btn-padding-y:0;--bs-btn-border-radius:6px;--bs-btn-hover-bg:#f3f4f6;--bs-btn-hover-border-color:#d1d5db;
  --bs-btn-active-bg:#e5e7eb;--bs-btn-active-border-color:#d1d5db}
.btn.ck-trigger{--bs-btn-hover-bg:#fff;--bs-btn-hover-color:var(--ck-muted);--bs-btn-active-bg:#fff;--bs-btn-active-color:var(--ck-muted);--bs-btn-active-border-color:var(--ck-ink)}
.btn.ck-esc:focus-visible,.btn.ck-trigger:focus-visible{box-shadow:none}
JavaScript
// Command palette. The dialog opens natively through command="show-modal" (invoker commands); this script adds
// the ⌘K / Ctrl+K shortcut, filtering, arrow-key movement and a fallback for browsers without invoker commands.
(() => {
  const dialog = document.getElementById('ck-dialog');

  document.querySelectorAll('[data-cmdk]').forEach(panel => {
    const input = panel.querySelector('[role="combobox"]');
    const opts = [...panel.querySelectorAll('[role="option"]')];
    const status = panel.querySelector('[role="status"]');
    const shown = () => opts.filter(o => !o.hidden);
    const current = () => opts.find(o => o.getAttribute('aria-selected') === 'true');

    function select(o) {
      opts.forEach(x => x.setAttribute('aria-selected', String(x === o)));
      if (o) { input.setAttribute('aria-activedescendant', o.id); o.scrollIntoView({ block: 'nearest' }); }
      else input.removeAttribute('aria-activedescendant');
    }
    function filter() {
      const q = input.value.trim().toLowerCase();
      opts.forEach(o => { o.hidden = q !== '' && !(o.dataset.label + ' ' + o.dataset.keywords).toLowerCase().includes(q); });
      const list = shown();
      select(list[0]);
      status.textContent = q ? list.length + (list.length === 1 ? ' result' : ' results') : '';
    }
    function run(o) {
      panel.dispatchEvent(new CustomEvent('cmdk:run', { bubbles: true, detail: o.dataset.label }));
      panel.closest('dialog')?.close();
    }

    input.addEventListener('input', filter);
    input.addEventListener('keydown', e => {
      const list = shown(), i = list.indexOf(current());
      if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
        e.preventDefault();
        if (list.length) select(list[(i + (e.key === 'ArrowDown' ? 1 : -1) + list.length) % list.length]);
      } else if (e.key === 'Enter') {
        e.preventDefault();
        if (i > -1) run(list[i]);
      }
    });
    panel.addEventListener('mousedown', e => { if (e.target.closest('[role="option"]')) e.preventDefault(); });
    panel.addEventListener('click', e => { const o = e.target.closest('[role="option"]'); if (o) run(o); });
    panel.closest('dialog')?.addEventListener('close', () => { input.value = ''; filter(); });
    filter();
  });

  if (!dialog) return;
  // ⌘K on Apple devices, Ctrl+K elsewhere
  if (!/Mac|iPhone|iPad/.test(navigator.platform)) document.querySelectorAll('[data-cmdk-key]').forEach(k => { k.textContent = 'Ctrl K'; });
  document.addEventListener('keydown', e => {
    if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); dialog.open ? dialog.close() : dialog.showModal(); }
  });
  // light dismiss where closedby="any" is not supported yet
  dialog.addEventListener('click', e => { if (e.target === dialog) dialog.close(); });
  // fallback for browsers without invoker commands (command / commandfor)
  if (!('command' in HTMLButtonElement.prototype)) {
    document.querySelectorAll('button[commandfor="ck-dialog"]').forEach(b => b.addEventListener('click', () => {
      b.getAttribute('command') === 'close' ? dialog.close() : dialog.showModal();
    }));
  }
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5b6477] uppercase">Opens with ⌘K or the button</span>
<button class="flex h-12 w-full max-w-[440px] cursor-pointer items-center gap-2.5 rounded-xl border border-ck-line bg-white pr-2.5 pl-3.5 text-left text-[.95rem] text-ck-muted shadow-[0_1px_2px_rgba(17,24,39,.05)] transition-[border-color,box-shadow] duration-150 hover:border-ck-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ck-accent motion-reduce:transition-none" type="button" command="show-modal" commandfor="ck-dialog" aria-keyshortcuts="Meta+K Control+K"><svg class="size-[18px] flex-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg><span class="min-w-0 flex-1 truncate">Search or jump to…</span><kbd class="inline-grid h-6 min-w-[22px] place-items-center rounded-md border border-[#d1d5db] border-b-2 bg-white px-2 font-mono text-[.75rem] leading-none font-semibold text-ck-text" data-cmdk-key aria-hidden="true">⌘K</kbd></button>
<p class="mt-3 text-[.88rem] text-ck-text" id="ck-out" aria-live="polite">Press ⌘K or Ctrl+K, or use the button.</p>
<dialog class="ck-dialog m-auto mt-[12vh] max-h-[min(520px,calc(100dvh_-_96px))] w-[min(640px,calc(100vw_-_32px))] overflow-visible border-0 bg-transparent p-0 backdrop:bg-[rgba(17,24,39,.45)] backdrop:backdrop-blur-[3px]" id="ck-dialog" aria-label="Command palette" closedby="any">
<div class="group/panel flex max-h-[inherit] flex-col overflow-hidden rounded-2xl border border-[#e5e7eb] bg-white text-ck-ink shadow-[0_1px_2px_rgba(17,24,39,.05),0_30px_70px_-20px_rgba(17,24,39,.35)]" data-cmdk>
  <form class="relative flex flex-none items-center gap-2.5 border-b border-[#e5e7eb] pr-3 pl-4" role="search" aria-label="Commands">
    <svg class="size-5 flex-none text-ck-muted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
    <label class="sr-only" for="ck-q">Search commands</label>
    <input class="h-14 min-w-0 flex-1 appearance-none border-0 bg-transparent text-[1.02rem] text-ck-ink outline-none placeholder:text-ck-muted [&::-webkit-search-cancel-button]:hidden" id="ck-q" type="search" placeholder="Type a command or search…" autocomplete="off" spellcheck="false" autofocus
      role="combobox" aria-expanded="true" aria-controls="ck-list" aria-autocomplete="list">
    <button class="h-[26px] flex-none cursor-pointer rounded-md border border-[#d1d5db] border-b-2 bg-white px-2 font-mono text-[.72rem] font-semibold text-ck-text hover:bg-[#f3f4f6] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ck-accent" type="button" command="close" commandfor="ck-dialog" aria-label="Close">Esc</button>
  </form>
  <div class="peer min-h-0 flex-1 overflow-y-auto overscroll-contain p-2" id="ck-list" role="listbox" aria-label="Commands">
    <div class="mb-1 last:mb-0 [&:not(:has([role=option]:not([hidden])))]:hidden" role="group" aria-labelledby="ck-g1">
      <div class="px-2.5 pt-2 pb-1.5 text-[.7rem] font-bold tracking-[.09em] text-ck-muted uppercase" id="ck-g1">Suggestions</div>
      <div class="group/opt flex h-11 cursor-pointer items-center gap-3 rounded-[10px] px-2.5 text-[.93rem] text-ck-text hover:bg-[#f9fafb] aria-selected:bg-ck-soft aria-selected:text-ck-ink [&[hidden]]:hidden" role="option" id="ck-o1" aria-selected="false" data-label="Create new issue" data-keywords="add ticket bug task"><span class="grid size-7 flex-none place-items-center rounded-lg border border-[#e5e7eb] bg-white text-ck-muted group-aria-selected/opt:border-[#ddd6fe] group-aria-selected/opt:text-ck-accent [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 5v14M5 12h14"/></svg></span><span class="min-w-0 flex-1 truncate">Create new issue</span><span class="flex flex-none gap-1" aria-hidden="true"><kbd class="inline-grid h-[22px] min-w-[22px] place-items-center rounded-md border border-[#d1d5db] border-b-2 bg-white px-1.5 font-mono text-[.72rem] leading-none font-semibold text-ck-text">C</kbd></span></div>
      <div class="group/opt flex h-11 cursor-pointer items-center gap-3 rounded-[10px] px-2.5 text-[.93rem] text-ck-text hover:bg-[#f9fafb] aria-selected:bg-ck-soft aria-selected:text-ck-ink [&[hidden]]:hidden" role="option" id="ck-o2" aria-selected="false" data-label="Search issues" data-keywords="find filter"><span class="grid size-7 flex-none place-items-center rounded-lg border border-[#e5e7eb] bg-white text-ck-muted group-aria-selected/opt:border-[#ddd6fe] group-aria-selected/opt:text-ck-accent [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg></span><span class="min-w-0 flex-1 truncate">Search issues</span><span class="flex flex-none gap-1" aria-hidden="true"><kbd class="inline-grid h-[22px] min-w-[22px] place-items-center rounded-md border border-[#d1d5db] border-b-2 bg-white px-1.5 font-mono text-[.72rem] leading-none font-semibold text-ck-text">/</kbd></span></div>
      <div class="group/opt flex h-11 cursor-pointer items-center gap-3 rounded-[10px] px-2.5 text-[.93rem] text-ck-text hover:bg-[#f9fafb] aria-selected:bg-ck-soft aria-selected:text-ck-ink [&[hidden]]:hidden" role="option" id="ck-o3" aria-selected="false" data-label="Go to inbox" data-keywords="notifications mentions"><span class="grid size-7 flex-none place-items-center rounded-lg border border-[#e5e7eb] bg-white text-ck-muted group-aria-selected/opt:border-[#ddd6fe] group-aria-selected/opt:text-ck-accent [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4 13h4l1.5 3h5L16 13h4"/><path d="M5.5 5h13L20 13v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-5Z"/></svg></span><span class="min-w-0 flex-1 truncate">Go to inbox</span><span class="flex flex-none gap-1" aria-hidden="true"><kbd class="inline-grid h-[22px] min-w-[22px] place-items-center rounded-md border border-[#d1d5db] border-b-2 bg-white px-1.5 font-mono text-[.72rem] leading-none font-semibold text-ck-text">G</kbd><kbd class="inline-grid h-[22px] min-w-[22px] place-items-center rounded-md border border-[#d1d5db] border-b-2 bg-white px-1.5 font-mono text-[.72rem] leading-none font-semibold text-ck-text">I</kbd></span></div>
    </div>
    <div class="mb-1 last:mb-0 [&:not(:has([role=option]:not([hidden])))]:hidden" role="group" aria-labelledby="ck-g2">
      <div class="px-2.5 pt-2 pb-1.5 text-[.7rem] font-bold tracking-[.09em] text-ck-muted uppercase" id="ck-g2">Settings</div>
      <div class="group/opt flex h-11 cursor-pointer items-center gap-3 rounded-[10px] px-2.5 text-[.93rem] text-ck-text hover:bg-[#f9fafb] aria-selected:bg-ck-soft aria-selected:text-ck-ink [&[hidden]]:hidden" role="option" id="ck-o4" aria-selected="false" data-label="Billing and plans" data-keywords="invoice payment subscription upgrade"><span class="grid size-7 flex-none place-items-center rounded-lg border border-[#e5e7eb] bg-white text-ck-muted group-aria-selected/opt:border-[#ddd6fe] group-aria-selected/opt:text-ck-accent [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 10h18M7 15h3"/></svg></span><span class="min-w-0 flex-1 truncate">Billing and plans</span></div>
      <div class="group/opt flex h-11 cursor-pointer items-center gap-3 rounded-[10px] px-2.5 text-[.93rem] text-ck-text hover:bg-[#f9fafb] aria-selected:bg-ck-soft aria-selected:text-ck-ink [&[hidden]]:hidden" role="option" id="ck-o5" aria-selected="false" data-label="Team members" data-keywords="invite people seats roles"><span class="grid size-7 flex-none place-items-center rounded-lg border border-[#e5e7eb] bg-white text-ck-muted group-aria-selected/opt:border-[#ddd6fe] group-aria-selected/opt:text-ck-accent [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="9" cy="8" r="3.5"/><path d="M3 20a6 6 0 0 1 12 0M16 4.5a3.5 3.5 0 0 1 0 7M21 20a6 6 0 0 0-3.5-5.4"/></svg></span><span class="min-w-0 flex-1 truncate">Team members</span></div>
      <div class="group/opt flex h-11 cursor-pointer items-center gap-3 rounded-[10px] px-2.5 text-[.93rem] text-ck-text hover:bg-[#f9fafb] aria-selected:bg-ck-soft aria-selected:text-ck-ink [&[hidden]]:hidden" role="option" id="ck-o6" aria-selected="false" data-label="API keys" data-keywords="tokens developer webhooks"><span class="grid size-7 flex-none place-items-center rounded-lg border border-[#e5e7eb] bg-white text-ck-muted group-aria-selected/opt:border-[#ddd6fe] group-aria-selected/opt:text-ck-accent [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="8" cy="15" r="4"/><path d="m11 12 9-9M17 6l3 3M15 8l2 2"/></svg></span><span class="min-w-0 flex-1 truncate">API keys</span></div>
    </div>
    <div class="mb-1 last:mb-0 [&:not(:has([role=option]:not([hidden])))]:hidden" role="group" aria-labelledby="ck-g3">
      <div class="px-2.5 pt-2 pb-1.5 text-[.7rem] font-bold tracking-[.09em] text-ck-muted uppercase" id="ck-g3">Help</div>
      <div class="group/opt flex h-11 cursor-pointer items-center gap-3 rounded-[10px] px-2.5 text-[.93rem] text-ck-text hover:bg-[#f9fafb] aria-selected:bg-ck-soft aria-selected:text-ck-ink [&[hidden]]:hidden" role="option" id="ck-o7" aria-selected="false" data-label="Keyboard shortcuts" data-keywords="hotkeys keys"><span class="grid size-7 flex-none place-items-center rounded-lg border border-[#e5e7eb] bg-white text-ck-muted group-aria-selected/opt:border-[#ddd6fe] group-aria-selected/opt:text-ck-accent [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 10h.01M10 10h.01M14 10h.01M18 10h.01M7 14h10"/></svg></span><span class="min-w-0 flex-1 truncate">Keyboard shortcuts</span><span class="flex flex-none gap-1" aria-hidden="true"><kbd class="inline-grid h-[22px] min-w-[22px] place-items-center rounded-md border border-[#d1d5db] border-b-2 bg-white px-1.5 font-mono text-[.72rem] leading-none font-semibold text-ck-text">?</kbd></span></div>
      <div class="group/opt flex h-11 cursor-pointer items-center gap-3 rounded-[10px] px-2.5 text-[.93rem] text-ck-text hover:bg-[#f9fafb] aria-selected:bg-ck-soft aria-selected:text-ck-ink [&[hidden]]:hidden" role="option" id="ck-o8" aria-selected="false" data-label="Contact support" data-keywords="help chat email"><span class="grid size-7 flex-none place-items-center rounded-lg border border-[#e5e7eb] bg-white text-ck-muted group-aria-selected/opt:border-[#ddd6fe] group-aria-selected/opt:text-ck-accent [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M9.5 9.5a2.5 2.5 0 1 1 3.5 2.3c-.6.3-1 .9-1 1.6V14M12 17h.01"/></svg></span><span class="min-w-0 flex-1 truncate">Contact support</span></div>
    </div>
  </div>
  <p class="hidden px-4 pt-7 pb-8 text-center text-[.92rem] text-ck-muted peer-[:not(:has([role=option]:not([hidden])))]:block">No commands match.</p>
  <div class="flex flex-none flex-wrap gap-x-4 gap-y-1 border-t border-[#e5e7eb] bg-[#f9fafb] px-4 py-2.5 text-[.78rem] text-ck-muted max-[480px]:hidden" aria-hidden="true"><span><kbd class="inline-grid h-5 min-w-5 place-items-center rounded-md border border-[#d1d5db] border-b-2 bg-white px-1.5 font-mono text-[.72rem] leading-none font-semibold text-ck-text mr-0.5">↑</kbd><kbd class="inline-grid h-5 min-w-5 place-items-center rounded-md border border-[#d1d5db] border-b-2 bg-white px-1.5 font-mono text-[.72rem] leading-none font-semibold text-ck-text mr-0.5">↓</kbd> move</span><span><kbd class="inline-grid h-5 min-w-5 place-items-center rounded-md border border-[#d1d5db] border-b-2 bg-white px-1.5 font-mono text-[.72rem] leading-none font-semibold text-ck-text mr-0.5">↵</kbd> run</span><span><kbd class="inline-grid h-5 min-w-5 place-items-center rounded-md border border-[#d1d5db] border-b-2 bg-white px-1.5 font-mono text-[.72rem] leading-none font-semibold text-ck-text mr-0.5">esc</kbd> close</span></div>
  <p class="sr-only" role="status"></p>
</div>
</dialog>
</div>

<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5b6477] uppercase">Inline quick switcher</span>
<div class="max-w-[560px]">
<div class="group/panel flex max-h-[inherit] flex-col overflow-hidden rounded-2xl border border-[#e5e7eb] bg-white text-ck-ink shadow-[0_1px_2px_rgba(17,24,39,.05),0_30px_70px_-20px_rgba(17,24,39,.35)]" data-cmdk>
  <form class="relative flex flex-none items-center gap-2.5 border-b border-[#e5e7eb] pr-3 pl-4" role="search" aria-label="Projects and people">
    <svg class="size-5 flex-none text-ck-muted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
    <label class="sr-only" for="ckp-q">Jump to a project or person</label>
    <input class="h-14 min-w-0 flex-1 appearance-none border-0 bg-transparent text-[1.02rem] text-ck-ink outline-none placeholder:text-ck-muted [&::-webkit-search-cancel-button]:hidden" id="ckp-q" type="search" placeholder="Jump to a project or person…" autocomplete="off" spellcheck="false"
      role="combobox" aria-expanded="true" aria-controls="ckp-list" aria-autocomplete="list">
  </form>
  <div class="peer min-h-0 flex-1 overflow-y-auto overscroll-contain p-2" id="ckp-list" role="listbox" aria-label="Projects and people">
    <div class="mb-1 last:mb-0 [&:not(:has([role=option]:not([hidden])))]:hidden" role="group" aria-labelledby="ckp-g1">
      <div class="px-2.5 pt-2 pb-1.5 text-[.7rem] font-bold tracking-[.09em] text-ck-muted uppercase" id="ckp-g1">Projects</div>
      <div class="group/opt flex h-11 cursor-pointer items-center gap-3 rounded-[10px] px-2.5 text-[.93rem] text-ck-text hover:bg-[#f9fafb] aria-selected:bg-ck-soft aria-selected:text-ck-ink [&[hidden]]:hidden" role="option" id="ckp-o1" aria-selected="false" data-label="Atlas redesign" data-keywords="website marketing"><span class="grid size-7 flex-none place-items-center rounded-lg border border-[#e5e7eb] bg-white text-ck-muted group-aria-selected/opt:border-[#ddd6fe] group-aria-selected/opt:text-ck-accent [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg></span><span class="min-w-0 flex-1 truncate">Atlas redesign</span></div>
      <div class="group/opt flex h-11 cursor-pointer items-center gap-3 rounded-[10px] px-2.5 text-[.93rem] text-ck-text hover:bg-[#f9fafb] aria-selected:bg-ck-soft aria-selected:text-ck-ink [&[hidden]]:hidden" role="option" id="ckp-o2" aria-selected="false" data-label="Billing v2" data-keywords="payments stripe"><span class="grid size-7 flex-none place-items-center rounded-lg border border-[#e5e7eb] bg-white text-ck-muted group-aria-selected/opt:border-[#ddd6fe] group-aria-selected/opt:text-ck-accent [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg></span><span class="min-w-0 flex-1 truncate">Billing v2</span></div>
      <div class="group/opt flex h-11 cursor-pointer items-center gap-3 rounded-[10px] px-2.5 text-[.93rem] text-ck-text hover:bg-[#f9fafb] aria-selected:bg-ck-soft aria-selected:text-ck-ink [&[hidden]]:hidden" role="option" id="ckp-o3" aria-selected="false" data-label="Mobile onboarding" data-keywords="ios android app"><span class="grid size-7 flex-none place-items-center rounded-lg border border-[#e5e7eb] bg-white text-ck-muted group-aria-selected/opt:border-[#ddd6fe] group-aria-selected/opt:text-ck-accent [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg></span><span class="min-w-0 flex-1 truncate">Mobile onboarding</span></div>
    </div>
    <div class="mb-1 last:mb-0 [&:not(:has([role=option]:not([hidden])))]:hidden" role="group" aria-labelledby="ckp-g2">
      <div class="px-2.5 pt-2 pb-1.5 text-[.7rem] font-bold tracking-[.09em] text-ck-muted uppercase" id="ckp-g2">People</div>
      <div class="group/opt flex h-11 cursor-pointer items-center gap-3 rounded-[10px] px-2.5 text-[.93rem] text-ck-text hover:bg-[#f9fafb] aria-selected:bg-ck-soft aria-selected:text-ck-ink [&[hidden]]:hidden" role="option" id="ckp-o4" aria-selected="false" data-label="Maya Reyes" data-keywords="design lead"><span class="grid size-7 flex-none place-items-center rounded-lg border border-[#e5e7eb] bg-white text-ck-muted group-aria-selected/opt:border-[#ddd6fe] group-aria-selected/opt:text-ck-accent [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg></span><span class="min-w-0 flex-1 truncate">Maya Reyes</span></div>
      <div class="group/opt flex h-11 cursor-pointer items-center gap-3 rounded-[10px] px-2.5 text-[.93rem] text-ck-text hover:bg-[#f9fafb] aria-selected:bg-ck-soft aria-selected:text-ck-ink [&[hidden]]:hidden" role="option" id="ckp-o5" aria-selected="false" data-label="Tomasz Nowak" data-keywords="backend engineer"><span class="grid size-7 flex-none place-items-center rounded-lg border border-[#e5e7eb] bg-white text-ck-muted group-aria-selected/opt:border-[#ddd6fe] group-aria-selected/opt:text-ck-accent [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg></span><span class="min-w-0 flex-1 truncate">Tomasz Nowak</span></div>
    </div>
  </div>
  <p class="hidden px-4 pt-7 pb-8 text-center text-[.92rem] text-ck-muted peer-[:not(:has([role=option]:not([hidden])))]:block">No projects or people match.</p>
  <p class="sr-only" role="status"></p>
</div>
</div></div>
CSS
@theme {
  --color-ck-ink: #111827;
  --color-ck-text: #374151;
  --color-ck-muted: #6b7280;
  --color-ck-line: #7c8196;
  --color-ck-accent: #7c3aed;
  --color-ck-soft: #f5f3ff;
}
/* <dialog> entry and exit: @starting-style animates it in, allow-discrete keeps it visible while it animates out */
@layer components {
  .ck-dialog { transition: opacity .18s ease, transform .18s ease, overlay .18s allow-discrete, display .18s allow-discrete; }
  .ck-dialog:not([open]) { opacity: 0; transform: translateY(-8px) scale(.98); }
  .ck-dialog::backdrop { transition: background-color .18s, overlay .18s allow-discrete, display .18s allow-discrete; }
  .ck-dialog:not([open])::backdrop { background: rgba(17,24,39,0); }
  @starting-style {
    .ck-dialog[open] { opacity: 0; transform: translateY(-8px) scale(.98); }
    .ck-dialog[open]::backdrop { background: rgba(17,24,39,0); }
  }
  @media (prefers-reduced-motion: reduce) {
    .ck-dialog, .ck-dialog::backdrop { transition: none; }
    .ck-dialog:not([open]) { transform: none; }
  }
}
JavaScript
// Command palette. The dialog opens natively through command="show-modal" (invoker commands); this script adds
// the ⌘K / Ctrl+K shortcut, filtering, arrow-key movement and a fallback for browsers without invoker commands.
(() => {
  const dialog = document.getElementById('ck-dialog');

  document.querySelectorAll('[data-cmdk]').forEach(panel => {
    const input = panel.querySelector('[role="combobox"]');
    const opts = [...panel.querySelectorAll('[role="option"]')];
    const status = panel.querySelector('[role="status"]');
    const shown = () => opts.filter(o => !o.hidden);
    const current = () => opts.find(o => o.getAttribute('aria-selected') === 'true');

    function select(o) {
      opts.forEach(x => x.setAttribute('aria-selected', String(x === o)));
      if (o) { input.setAttribute('aria-activedescendant', o.id); o.scrollIntoView({ block: 'nearest' }); }
      else input.removeAttribute('aria-activedescendant');
    }
    function filter() {
      const q = input.value.trim().toLowerCase();
      opts.forEach(o => { o.hidden = q !== '' && !(o.dataset.label + ' ' + o.dataset.keywords).toLowerCase().includes(q); });
      const list = shown();
      select(list[0]);
      status.textContent = q ? list.length + (list.length === 1 ? ' result' : ' results') : '';
    }
    function run(o) {
      panel.dispatchEvent(new CustomEvent('cmdk:run', { bubbles: true, detail: o.dataset.label }));
      panel.closest('dialog')?.close();
    }

    input.addEventListener('input', filter);
    input.addEventListener('keydown', e => {
      const list = shown(), i = list.indexOf(current());
      if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
        e.preventDefault();
        if (list.length) select(list[(i + (e.key === 'ArrowDown' ? 1 : -1) + list.length) % list.length]);
      } else if (e.key === 'Enter') {
        e.preventDefault();
        if (i > -1) run(list[i]);
      }
    });
    panel.addEventListener('mousedown', e => { if (e.target.closest('[role="option"]')) e.preventDefault(); });
    panel.addEventListener('click', e => { const o = e.target.closest('[role="option"]'); if (o) run(o); });
    panel.closest('dialog')?.addEventListener('close', () => { input.value = ''; filter(); });
    filter();
  });

  if (!dialog) return;
  // ⌘K on Apple devices, Ctrl+K elsewhere
  if (!/Mac|iPhone|iPad/.test(navigator.platform)) document.querySelectorAll('[data-cmdk-key]').forEach(k => { k.textContent = 'Ctrl K'; });
  document.addEventListener('keydown', e => {
    if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); dialog.open ? dialog.close() : dialog.showModal(); }
  });
  // light dismiss where closedby="any" is not supported yet
  dialog.addEventListener('click', e => { if (e.target === dialog) dialog.close(); });
  // fallback for browsers without invoker commands (command / commandfor)
  if (!('command' in HTMLButtonElement.prototype)) {
    document.querySelectorAll('button[commandfor="ck-dialog"]').forEach(b => b.addEventListener('click', () => {
      b.getAttribute('command') === 'close' ? dialog.close() : dialog.showModal();
    }));
  }
})();

About this set

A command palette in the style of modern web apps, built with current platform features instead of a modal library. A trigger that looks like a search field opens a dialog element as a modal through invoker commands, command=”show-modal” and commandfor, so opening, the backdrop, focus trapping and Escape to close all come from the browser. @starting-style with transition-behavior: allow-discrete fades and lifts the dialog in and out, and the backdrop is blurred. Inside, a combobox filters a grouped listbox of commands such as Create new issue, Billing and plans and Keyboard shortcuts. :has() does the bookkeeping: a group disappears when none of its options match, and a no-results message appears when nothing does. A short script adds the Cmd K or Ctrl K shortcut, the filtering, arrow-key movement and Enter to run, and falls back to showModal() where invoker commands are not supported. The same panel also works inline as a quick switcher for projects and people, shown below the trigger.

What’s included

  • Modal dialog opened with command and commandfor, no modal library
  • Entry and exit animation with @starting-style and allow-discrete
  • Grouped listbox; :has() hides empty groups and shows a no-results message
  • Cmd K or Ctrl K shortcut, arrow keys and Enter
  • Same panel works inline as a quick switcher

Accessibility

The dialog has an accessible name and is modal, so focus stays inside until it closes with Escape, the Esc button or a click on the backdrop. The input is a combobox named by a hidden label inside a form with role="search"; options live in labelled groups and the active one is tracked with aria-activedescendant and aria-selected. A status message announces the number of results. Buttons show a 2px outline on focus and the animations are removed under prefers-reduced-motion.

Customise it

Change --ck-accent, --ck-soft and --ck-line on .ck-panel and .ck-trigger for colour, and the width and max-height on .ck-dialog for size. Replace the options with your own commands and listen for the cmdk:run event to act on them. In Tailwind edit the --color-ck-* theme colours.