Command — <mv-command>

Command palette: accent-insensitive fuzzy search (keywords included), ranked and grouped results, keyboard navigation with aria-activedescendant; standalone or inside a Dialog opened with ⌘K.

CategoryOverlays
TypeWeb Component (<mv-command>)
Statusstable
Keywordscommand, command-palette, cmdk, search, combobox, spotlight, launcher

When to use

Avoid when

Install

node scripts/add.mjs command --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["command"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, components/command/command.js, components/command/command.css.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div id="command-demo" style="display:grid;gap:1.25rem;justify-items:center;width:min(28rem,100%)">
  <mv-command label="Quick search">
    <input placeholder="Type a command or search…">
    <div class="mv-command-list">
      <div class="mv-command-group" data-label="Suggestions">
        <div class="mv-command-item" data-value="calendar" data-keywords="agenda schedule meetings">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>
          Calendar
        </div>
        <div class="mv-command-item" data-value="emoji" data-keywords="smiley emoticon">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01"/></svg>
          Search emoji
        </div>
        <div class="mv-command-item" data-value="calculator" data-disabled>
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="2" width="16" height="20" rx="2"/><path d="M8 6h8M8 14h.01M12 14h.01M16 14h.01M8 18h.01M12 18h.01M16 18h.01"/></svg>
          Calculator
        </div>
      </div>
      <hr class="mv-command-separator">
      <div class="mv-command-group" data-label="Settings">
        <div class="mv-command-item" data-value="profile" data-keywords="account avatar">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg>
          Profile <span class="mv-command-shortcut"><kbd>⌘</kbd><kbd>P</kbd></span>
        </div>
        <div class="mv-command-item" data-value="billing" data-keywords="payment subscription card">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="5" width="20" height="14" rx="2"/><path d="M2 10h20"/></svg>
          Billing <span class="mv-command-shortcut"><kbd>⌘</kbd><kbd>B</kbd></span>
        </div>
        <div class="mv-command-item" data-value="settings" data-keywords="preferences options">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3M1 14h6M9 8h6M17 16h6"/></svg>
          Settings <span class="mv-command-shortcut"><kbd>⌘</kbd><kbd>,</kbd></span>
        </div>
      </div>
    </div>
    <div class="mv-command-empty">No results for this search.</div>
  </mv-command>

  <div style="display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;justify-content:center">
    <mv-dialog>
      <button class="mv-button" data-variant="outline" data-mv-open>
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
        Search…
        <span data-role="hotkey" style="margin-left:.5rem;padding:.125rem .375rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-xs);background:var(--mv-bg-subtle);color:var(--mv-fg-muted);font-size:.6875rem">Ctrl K</span>
      </button>
      <dialog class="mv-command-dialog" aria-label="Command palette">
        <mv-command label="Command palette">
          <input placeholder="Search pages and actions…" autofocus>
          <div class="mv-command-list">
            <div class="mv-command-group" data-label="Navigation">
              <div class="mv-command-item" data-value="dashboard" data-keywords="home overview">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="9" rx="1"/><rect x="14" y="3" width="7" height="5" rx="1"/><rect x="14" y="12" width="7" height="9" rx="1"/><rect x="3" y="16" width="7" height="5" rx="1"/></svg>
                Dashboard <span class="mv-command-item-meta">Home</span>
              </div>
              <div class="mv-command-item" data-value="projects" data-keywords="folders work">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><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>
                Projects
              </div>
              <div class="mv-command-item" data-value="invoices" data-keywords="billing payments quotes">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 2h9l5 5v15H6z"/><path d="M14 2v6h6M9 13h6M9 17h4"/></svg>
                Invoices
              </div>
            </div>
            <div class="mv-command-group" data-label="Actions">
              <div class="mv-command-item" data-value="new-project" data-keywords="create add">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>
                New project <span class="mv-command-shortcut"><kbd>⌘</kbd><kbd>N</kbd></span>
              </div>
              <div class="mv-command-item" data-value="invite" data-keywords="team member collaborator share">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="9" cy="8" r="4"/><path d="M2 21a7 7 0 0 1 14 0M19 8v6M16 11h6"/></svg>
                Invite a member
              </div>
              <div class="mv-command-item" data-value="theme" data-keywords="dark light mode appearance">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
                Toggle theme <span class="mv-command-shortcut"><kbd>⇧</kbd><kbd>⌘</kbd><kbd>L</kbd></span>
              </div>
            </div>
            <div class="mv-command-group" data-label="Help">
              <div class="mv-command-item" data-value="documentation" data-keywords="guide docs help">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>
                Documentation
              </div>
              <div class="mv-command-item" data-value="support" data-keywords="contact help assistance">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
                Contact support
              </div>
            </div>
          </div>
          <div class="mv-command-empty">No results. Try “project” or “theme”.</div>
          <div class="mv-command-footer" aria-hidden="true">
            <span><kbd><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg></kbd><kbd><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M19 12l-7 7-7-7"/></svg></kbd> navigate</span>
            <span><kbd><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 4v7a4 4 0 0 1-4 4H4M9 10l-5 5 5 5"/></svg></kbd> open</span>
            <span style="margin-left:auto"><kbd>Esc</kbd> close</span>
          </div>
        </mv-command>
      </dialog>
    </mv-dialog>
  </div>
  <p data-role="out" aria-live="polite" style="margin:0;font-size:.8125rem;color:var(--mv-fg-muted)">Nothing selected yet.</p>
</div>
<script type="module">
  const root = document.getElementById("command-demo");
  const out = root.querySelector('[data-role="out"]');
  const host = root.querySelector("mv-dialog");
  const dialog = host.querySelector("dialog");
  const palette = dialog.querySelector("mv-command");
  const mac = /Mac|iPhone|iPad/.test(navigator.platform);
  if (mac) root.querySelector('[data-role="hotkey"]').textContent = "⌘ K";

  root.addEventListener("mv-select", (e) => {
    out.textContent = `Selected: ${e.detail.label}`;
    if (dialog.contains(e.target)) host.close(e.detail.value);
  });
  dialog.addEventListener("close", () => palette.clear());

  // ⌘K / Ctrl+K toggles the palette (the listener removes itself when the demo is gone).
  document.addEventListener("keydown", function onKey(e) {
    if (!root.isConnected) return document.removeEventListener("keydown", onKey);
    if (e.key.toLowerCase() !== "k" || !(e.metaKey || e.ctrlKey) || e.altKey || e.shiftKey) return;
    e.preventDefault();
    dialog.open ? host.close() : host.show();
  });
</script>

API

Attributes

NameTypeDefaultDescription
labelstringCommandsAccessible name of the list (and of the input if it has no label).
manualbooleanDisables built-in filtering (async results): listen to mv-filter and replace the items — they are picked up automatically.

Properties

NameTypeDescription
querystringSearch text (read/write; writing re-runs the filter).
activeItemElement | nullCurrently active item.

Methods

NameDescription
clear()Clears the search.
focus()Moves focus to the input.

Events

NameDescription
mv-selectEnter or click. detail: { value, label, item }.
mv-filterAfter each filter pass. detail: { query, count }.

Content structure

NameDescription
<input>Search input (automatically wrapped with a magnifier icon).
.mv-command-listList (role=listbox).
.mv-command-group[data-label]Titled group; hidden when it has no matching results.
.mv-command-itemOption: data-value (→ detail.value), data-keywords (hidden searchable terms), data-disabled, data-always (always shown).
.mv-command-separatorSeparator, hidden while searching.
.mv-command-emptyEmpty state, announced (role=status).

CSS classes

NameDescription
mv-command-shortcutRight-aligned shortcut; its <kbd> elements render as keys.
mv-command-item-metaSubtle secondary text inside an item.
mv-command-footerFooter hint bar (↑↓ ↵ Esc keys).
mv-command-dialogOn an mv-dialog’s <dialog>: palette at the top of the screen, no inner padding.

CSS variables

NameDefaultDescription
--mv-command-height20remMax height of the list.
--mv-command-item-height2.25remMin height of an item.
--mv-command-highlightActive item background.
--mv-command-bgBackground.
--mv-command-radiusRadius.
--mv-command-dialog-width36remWidth inside a dialog.

Accessibility

APG “Combobox” pattern with an always-visible listbox popup: focus stays in the input (role=combobox, aria-controls, aria-activedescendant), role=option items with aria-selected / aria-disabled, role=group groups named by their title. ↑↓ (wrapping), Ctrl+N/P or Ctrl+J/K, Page Up/Down, Ctrl+Home/End, Enter confirms; Home/End stay in the text. Esc first clears the search, then closes the dialog. The empty state is announced politely.