Skip to content
data-slotv1.0.0
Esc
↑↓navigate↵open⌘Jpreview
On this page

Command

A searchable command list for navigating actions with the keyboard.

@data-slot/commandSource ↗
bun add @data-slot/command
npm install @data-slot/command
pnpm add @data-slot/command

Anatomy

Item values are inferred from their text, so command-shortcut hints never affect search matches. command-empty appears when nothing ranks.

<div data-slot="command" data-label="Command Menu">
  <div data-slot="command-input-wrapper">
    <input data-slot="command-input" placeholder="Type a command..." />
  </div>
  <div data-slot="command-list">
    <div data-slot="command-empty" hidden>No results.</div>

    <div data-slot="command-group">
      <div data-slot="command-group-heading">Actions</div>
      <div data-slot="command-item">Open Project</div>
      <div data-slot="command-item">Invite Teammate</div>
    </div>

    <div data-slot="command-separator"></div>

    <div data-slot="command-item" data-value="settings">
      Settings
      <span data-slot="command-shortcut">⌘,</span>
    </div>
  </div>
</div>

Examples

The modal palette examples also need @data-slot/dialog:

npm install @data-slot/command @data-slot/dialog

Inline command list

Preview
⌕
Project
Open Dashboard↵
Invite Teammate⌘I
Preferences
Open Settings⌘,
Billing PortalSoon
⌕
Project
Open Dashboard↵
Invite Teammate⌘I
Preferences
Open Settings⌘,
Billing PortalSoon

Inline palettes work as always-open searchable lists. Active state is exposed throughdata-selected andaria-selected. Keyboard navigation is handled at the palette root, without forcing focus into the input on generic box clicks.

Show codeHide code
<label for="command-basic-command-inline" class="command-field-label">Workspace</label>
<div data-slot="command" data-label="Workspace Command Menu" class="command-root">
  <div data-slot="command-input-wrapper" class="command-input-shell">
    <span class="command-search-icon">⌕</span>
    <input
      data-slot="command-input"
      id="command-basic-command-inline"
      class="command-input"
      placeholder="Search commands..."
    />
  </div>
  <div data-slot="command-list" class="command-list">
    <div data-slot="command-empty" class="command-empty" hidden>No results.</div>

    <div data-slot="command-group" class="command-group">
      <div data-slot="command-group-heading" class="command-heading">Project</div>
      <div data-slot="command-item" class="command-item">
        Open Dashboard
        <span data-slot="command-shortcut" class="command-shortcut">↵</span>
      </div>
      <div data-slot="command-item" data-keywords="invite,team" class="command-item">
        Invite Teammate
        <span data-slot="command-shortcut" class="command-shortcut">⌘I</span>
      </div>
    </div>

    <div data-slot="command-separator" class="command-separator"></div>

    <div data-slot="command-group" class="command-group">
      <div data-slot="command-group-heading" class="command-heading">Preferences</div>
      <div data-slot="command-item" data-value="settings" class="command-item">
        Open Settings
        <span data-slot="command-shortcut" class="command-shortcut">⌘,</span>
      </div>
      <div data-slot="command-item" data-disabled class="command-item command-item-disabled">
        Billing Portal
        <span data-slot="command-shortcut" class="command-shortcut">Soon</span>
      </div>
    </div>
  </div>
</div>

<style>
  .command-root {
    width: 100%;
    border: 1px solid var(--border);
    background: linear-gradient(in oklab, var(--bg), var(--code-bg));
    box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
    max-width: 28rem;
  }

  .command-root-dialog {
    width: 100%;
    max-width: none;
    overflow: hidden;
    box-shadow: none;
  }

  .command-field-label {
    display: block;
    margin-bottom: 0.5rem;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--muted);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    line-height: 1rem;
  }

  .command-input-shell {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.75rem 1rem;
    border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
  }

  .command-search-icon {
    color: var(--muted);
    font-size: 0.875rem;
    line-height: 1.25rem;
  }

  .command-input {
    width: 100%;
    border: none;
    background: transparent;
    font-size: 1rem;
    color: var(--text);
    outline: none;
    line-height: 1.5rem;
  }

  .command-input::placeholder {
    color: var(--muted);
  }

  .command-list {
    max-height: 18rem;
    overflow: auto;
    padding: 0.375rem;
  }

  .command-heading {
    padding: 0.5rem 0.75rem;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--muted);
    line-height: 1rem;
  }

  .command-item {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.625rem 0.75rem;
    color: var(--text);
    cursor: default;
    font-size: 0.875rem;
    line-height: 1.25rem;
  }

  .command-item[data-selected] {
    background: var(--code-bg);
  }

  .command-shortcut {
    margin-left: auto;
    color: var(--muted);
    font-size: 0.75rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    line-height: 1rem;
  }

  .command-item-disabled {
    color: var(--muted);
  }

  .command-separator {
    height: 1px;
    margin: 0.25rem 0;
    background: color-mix(in srgb, var(--border) 70%, transparent);
  }

  .command-empty {
    padding: 1rem 0.75rem;
    text-align: center;
    color: var(--muted);
    font-size: 0.875rem;
    line-height: 1.25rem;
  }

  .command-dialog-trigger {
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    padding: 0.5rem 1rem;
    font-size: 0.875rem;
    font-weight: 500;
    line-height: 1.25rem;
    transition: opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1);
  }

  .command-dialog-trigger:hover { opacity: 0.9; }

  .command-dialog-overlay {
    position: fixed;
    inset: 0;
    background: rgb(0 0 0 / 0.45);
    opacity: 0;
    transition: opacity 0.2s ease;
    z-index: 50;
  }

  .command-dialog-overlay[data-open] {
    opacity: 1;
  }

  .command-dialog-overlay[data-starting-style],
  .command-dialog-overlay[data-ending-style] {
    opacity: 0;
  }

  .command-dialog-panel {
    position: fixed;
    top: 16.666667%;
    left: 50%;
    transform: translateX(-50%);
    width: 91.666667%;
    padding: 1rem;
    border: 1px solid var(--border);
    background: var(--code-bg);
    box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
    opacity: 0;
    transition:
      opacity 0.2s ease,
      transform 0.2s ease;
    z-index: 60;
    max-width: 36rem;
  }

  .command-dialog-panel[data-open] {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }

  .command-dialog-panel[data-starting-style],
  .command-dialog-panel[data-ending-style] {
    opacity: 0;
    transform: translateX(-50%) translateY(0.5rem);
  }

  .command-dialog-title {
    margin: 0 0 0.25rem;
    font-size: 1rem;
    font-weight: 700;
    color: var(--text);
    line-height: 1.5rem;
  }

  .command-dialog-description {
    margin: 0 0 1rem;
    color: var(--muted);
    font-size: 0.875rem;
    line-height: 1.25rem;
  }
  .command-item-disabled .command-shortcut { color: inherit; }
</style>
<label for="command-basic-command-inline" class="mb-2 block text-xs font-semibold uppercase tracking-wider text-[var(--muted)]">
  Workspace
</label>
<div
  data-slot="command"
  data-label="Workspace Command Menu"
  class="w-full max-w-md border border-[var(--border)] bg-linear-to-b from-[var(--bg)] to-[var(--code-bg)] shadow-xl"
>
  <div
    data-slot="command-input-wrapper"
    class="flex items-center gap-2 border-b border-[var(--border)]/60 px-4 py-3"
  >
    <span class="text-sm text-[var(--muted)]">⌕</span>
    <input
      data-slot="command-input"
      id="command-basic-command-inline"
      class="w-full border-none bg-transparent text-base text-[var(--text)] outline-none placeholder:text-[var(--muted)]"
      placeholder="Search commands..."
    />
  </div>
  <div data-slot="command-list" class="max-h-72 overflow-auto p-1.5">
    <div data-slot="command-empty" class="px-3 py-4 text-center text-sm text-[var(--muted)]" hidden>No results.</div>
    <div data-slot="command-group">
      <div data-slot="command-group-heading" class="px-3 py-2 text-xs font-bold uppercase tracking-wider text-[var(--muted)]">
        Project
      </div>
      <div
        data-slot="command-item"
        class="flex cursor-default items-center gap-3 px-3 py-2.5 text-sm text-[var(--text)] data-selected:bg-[var(--code-bg)]"
      >
        Open Dashboard
        <span data-slot="command-shortcut" class="ml-auto text-xs uppercase tracking-wider text-[var(--muted)]">↵</span>
      </div>
      <div
        data-slot="command-item"
        data-keywords="invite,team"
        class="flex cursor-default items-center gap-3 px-3 py-2.5 text-sm text-[var(--text)] data-selected:bg-[var(--code-bg)]"
      >
        Invite Teammate
        <span data-slot="command-shortcut" class="ml-auto text-xs uppercase tracking-wider text-[var(--muted)]">⌘I</span>
      </div>
    </div>
    <div data-slot="command-separator" class="my-1 h-px bg-[var(--border)]/70"></div>
    <div data-slot="command-group">
      <div data-slot="command-group-heading" class="px-3 py-2 text-xs font-bold uppercase tracking-wider text-[var(--muted)]">
        Preferences
      </div>
      <div
        data-slot="command-item"
        data-value="settings"
        class="flex cursor-default items-center gap-3 px-3 py-2.5 text-sm text-[var(--text)] data-selected:bg-[var(--code-bg)]"
      >
        Open Settings
        <span data-slot="command-shortcut" class="ml-auto text-xs uppercase tracking-wider text-[var(--muted)]">⌘,</span>
      </div>
      <div
        data-slot="command-item"
        data-disabled
        class="flex cursor-default items-center gap-3 px-3 py-2.5 text-sm text-[var(--muted)]"
      >
        Billing Portal
        <span data-slot="command-shortcut" class="ml-auto text-xs uppercase tracking-wider text-[var(--muted)]">Soon</span>
      </div>
    </div>
  </div>
</div>
Preview

Compose @data-slot/command insidedialog-content to build modal palettes with the same strict cmdk-style focus behavior.

Show codeHide code
<div data-slot="dialog" class="command-dialog">
  <button data-slot="dialog-trigger" class="command-dialog-trigger">Open Command Palette</button>
  <div data-slot="dialog-overlay" class="command-dialog-overlay" hidden></div>
  <div data-slot="dialog-content" class="command-dialog-panel" hidden>
    <h2 data-slot="dialog-title" class="command-dialog-title">Command Palette</h2>
    <p data-slot="dialog-description" class="command-dialog-description">
      Search for a command to run across your workspace.
    </p>

    <div data-slot="command" data-label="Global Command Palette" class="command-root command-root-dialog">
      <div data-slot="command-input-wrapper" class="command-input-shell">
        <span class="command-search-icon">⌕</span>
        <input data-slot="command-input" class="command-input" placeholder="Search commands..." />
      </div>
      <div data-slot="command-list" class="command-list">
        <div data-slot="command-empty" class="command-empty" hidden>No results.</div>
        <div data-slot="command-item" class="command-item">
          New Issue
          <span data-slot="command-shortcut" class="command-shortcut">⌘N</span>
        </div>
        <div data-slot="command-item" class="command-item">
          Jump to Inbox
          <span data-slot="command-shortcut" class="command-shortcut">G I</span>
        </div>
        <div data-slot="command-item" class="command-item">
          Open Settings
          <span data-slot="command-shortcut" class="command-shortcut">⌘,</span>
        </div>
      </div>
    </div>
  </div>
</div>

<style>
  .command-root {
    width: 100%;
    border: 1px solid var(--border);
    background: linear-gradient(in oklab, var(--bg), var(--code-bg));
    box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
    max-width: 28rem;
  }

  .command-root-dialog {
    width: 100%;
    max-width: none;
    overflow: hidden;
    box-shadow: none;
  }

  .command-field-label {
    display: block;
    margin-bottom: 0.5rem;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--muted);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    line-height: 1rem;
  }

  .command-input-shell {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.75rem 1rem;
    border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
  }

  .command-search-icon {
    color: var(--muted);
    font-size: 0.875rem;
    line-height: 1.25rem;
  }

  .command-input {
    width: 100%;
    border: none;
    background: transparent;
    font-size: 1rem;
    color: var(--text);
    outline: none;
    line-height: 1.5rem;
  }

  .command-input::placeholder {
    color: var(--muted);
  }

  .command-list {
    max-height: 18rem;
    overflow: auto;
    padding: 0.375rem;
  }

  .command-heading {
    padding: 0.5rem 0.75rem;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--muted);
    line-height: 1rem;
  }

  .command-item {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.625rem 0.75rem;
    color: var(--text);
    cursor: default;
    font-size: 0.875rem;
    line-height: 1.25rem;
  }

  .command-item[data-selected] {
    background: var(--code-bg);
  }

  .command-shortcut {
    margin-left: auto;
    color: var(--muted);
    font-size: 0.75rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    line-height: 1rem;
  }

  .command-item-disabled {
    color: var(--muted);
  }

  .command-separator {
    height: 1px;
    margin: 0.25rem 0;
    background: color-mix(in srgb, var(--border) 70%, transparent);
  }

  .command-empty {
    padding: 1rem 0.75rem;
    text-align: center;
    color: var(--muted);
    font-size: 0.875rem;
    line-height: 1.25rem;
  }

  .command-dialog-trigger {
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    padding: 0.5rem 1rem;
    font-size: 0.875rem;
    font-weight: 500;
    line-height: 1.25rem;
    transition: opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1);
  }

  .command-dialog-trigger:hover { opacity: 0.9; }

  .command-dialog-overlay {
    position: fixed;
    inset: 0;
    background: rgb(0 0 0 / 0.45);
    opacity: 0;
    transition: opacity 0.2s ease;
    z-index: 50;
  }

  .command-dialog-overlay[data-open] {
    opacity: 1;
  }

  .command-dialog-overlay[data-starting-style],
  .command-dialog-overlay[data-ending-style] {
    opacity: 0;
  }

  .command-dialog-panel {
    position: fixed;
    top: 16.666667%;
    left: 50%;
    transform: translateX(-50%);
    width: 91.666667%;
    padding: 1rem;
    border: 1px solid var(--border);
    background: var(--code-bg);
    box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
    opacity: 0;
    transition:
      opacity 0.2s ease,
      transform 0.2s ease;
    z-index: 60;
    max-width: 36rem;
  }

  .command-dialog-panel[data-open] {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }

  .command-dialog-panel[data-starting-style],
  .command-dialog-panel[data-ending-style] {
    opacity: 0;
    transform: translateX(-50%) translateY(0.5rem);
  }

  .command-dialog-title {
    margin: 0 0 0.25rem;
    font-size: 1rem;
    font-weight: 700;
    color: var(--text);
    line-height: 1.5rem;
  }

  .command-dialog-description {
    margin: 0 0 1rem;
    color: var(--muted);
    font-size: 0.875rem;
    line-height: 1.25rem;
  }
  .command-item-disabled .command-shortcut { color: inherit; }
</style>
<div data-slot="dialog">
  <button
    data-slot="dialog-trigger"
    class="border border-[var(--border)] bg-[var(--surface)] px-4 py-2 text-sm font-medium text-text transition hover:opacity-90"
  >
    Open Command Palette
  </button>
  <div
    data-slot="dialog-overlay"
    class="fixed inset-0 z-50 bg-black/45 opacity-0 transition-opacity duration-200 data-open:opacity-100 data-starting-style:opacity-0 data-ending-style:opacity-0"
    hidden
  ></div>
  <div
    data-slot="dialog-content"
    class="fixed left-1/2 top-1/6 z-60 w-11/12 max-w-xl -translate-x-1/2 border border-[var(--border)] bg-[var(--code-bg)] p-4 opacity-0 shadow-2xl transition-all duration-200 data-open:opacity-100 data-starting-style:translate-y-2 data-starting-style:opacity-0 data-ending-style:translate-y-2 data-ending-style:opacity-0"
    hidden
  >
    <h2 data-slot="dialog-title" class="mb-1 text-base font-bold text-[var(--text)]">Command Palette</h2>
    <p data-slot="dialog-description" class="mb-4 text-sm text-[var(--muted)]">Search for a command to run across your workspace.</p>

    <div
      data-slot="command"
      data-label="Global Command Palette"
      class="overflow-hidden border border-[var(--border)] bg-linear-to-b from-[var(--bg)] to-[var(--code-bg)]"
    >
      <div data-slot="command-input-wrapper" class="flex items-center gap-2 border-b border-[var(--border)]/60 px-4 py-3">
        <span class="text-sm text-[var(--muted)]">⌕</span>
        <input
          data-slot="command-input"
          class="w-full border-none bg-transparent text-base text-[var(--text)] outline-none placeholder:text-[var(--muted)]"
          placeholder="Search commands..."
        />
      </div>
      <div data-slot="command-list" class="max-h-72 overflow-auto p-1.5">
        <div data-slot="command-empty" class="px-3 py-4 text-center text-sm text-[var(--muted)]" hidden>No results.</div>
        <div
          data-slot="command-item"
          class="flex cursor-default items-center gap-3 px-3 py-2.5 text-sm text-[var(--text)] data-selected:bg-[var(--code-bg)]"
        >
          New Issue
          <span data-slot="command-shortcut" class="ml-auto text-xs uppercase tracking-wider text-[var(--muted)]">⌘N</span>
        </div>
        <div
          data-slot="command-item"
          class="flex cursor-default items-center gap-3 px-3 py-2.5 text-sm text-[var(--text)] data-selected:bg-[var(--code-bg)]"
        >
          Jump to Inbox
          <span data-slot="command-shortcut" class="ml-auto text-xs uppercase tracking-wider text-[var(--muted)]">G I</span>
        </div>
        <div
          data-slot="command-item"
          class="flex cursor-default items-center gap-3 px-3 py-2.5 text-sm text-[var(--text)] data-selected:bg-[var(--code-bg)]"
        >
          Open Settings
          <span data-slot="command-shortcut" class="ml-auto text-xs uppercase tracking-wider text-[var(--muted)]">⌘,</span>
        </div>
      </div>
    </div>
  </div>
</div>

Loop keyboard navigation

With data-loop, pressing Down on the last item wraps back to the first.

Preview
⌕
Project
Open Dashboard↵
Invite Teammate⌘I
Preferences
Open Settings⌘,
Billing PortalSoon
⌕
Project
Open Dashboard↵
Invite Teammate⌘I
Preferences
Open Settings⌘,
Billing PortalSoon

Inline palettes work as always-open searchable lists. Active state is exposed throughdata-selected andaria-selected. Keyboard navigation is handled at the palette root, without forcing focus into the input on generic box clicks.

Show codeHide code
<label for="command-extra-command-inline" class="command-field-label">Workspace</label>
<div data-slot="command" data-loop data-label="Workspace Command Menu" class="command-root">
  <div data-slot="command-input-wrapper" class="command-input-shell">
    <span class="command-search-icon">⌕</span>
    <input
      data-slot="command-input"
      id="command-extra-command-inline"
      class="command-input"
      placeholder="Search commands..."
    />
  </div>
  <div data-slot="command-list" class="command-list">
    <div data-slot="command-empty" class="command-empty" hidden>No results.</div>

    <div data-slot="command-group" class="command-group">
      <div data-slot="command-group-heading" class="command-heading">Project</div>
      <div data-slot="command-item" class="command-item">
        Open Dashboard
        <span data-slot="command-shortcut" class="command-shortcut">↵</span>
      </div>
      <div data-slot="command-item" data-keywords="invite,team" class="command-item">
        Invite Teammate
        <span data-slot="command-shortcut" class="command-shortcut">⌘I</span>
      </div>
    </div>

    <div data-slot="command-separator" class="command-separator"></div>

    <div data-slot="command-group" class="command-group">
      <div data-slot="command-group-heading" class="command-heading">Preferences</div>
      <div data-slot="command-item" data-value="settings" class="command-item">
        Open Settings
        <span data-slot="command-shortcut" class="command-shortcut">⌘,</span>
      </div>
      <div data-slot="command-item" data-disabled class="command-item command-item-disabled">
        Billing Portal
        <span data-slot="command-shortcut" class="command-shortcut">Soon</span>
      </div>
    </div>
  </div>
</div>

<style>
  .command-root {
    width: 100%;
    border: 1px solid var(--border);
    background: linear-gradient(in oklab, var(--bg), var(--code-bg));
    box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
    max-width: 28rem;
  }

  .command-root-dialog {
    width: 100%;
    max-width: none;
    overflow: hidden;
    box-shadow: none;
  }

  .command-field-label {
    display: block;
    margin-bottom: 0.5rem;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--muted);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    line-height: 1rem;
  }

  .command-input-shell {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.75rem 1rem;
    border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
  }

  .command-search-icon {
    color: var(--muted);
    font-size: 0.875rem;
    line-height: 1.25rem;
  }

  .command-input {
    width: 100%;
    border: none;
    background: transparent;
    font-size: 1rem;
    color: var(--text);
    outline: none;
    line-height: 1.5rem;
  }

  .command-input::placeholder {
    color: var(--muted);
  }

  .command-list {
    max-height: 18rem;
    overflow: auto;
    padding: 0.375rem;
  }

  .command-heading {
    padding: 0.5rem 0.75rem;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--muted);
    line-height: 1rem;
  }

  .command-item {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.625rem 0.75rem;
    color: var(--text);
    cursor: default;
    font-size: 0.875rem;
    line-height: 1.25rem;
  }

  .command-item[data-selected] {
    background: var(--code-bg);
  }

  .command-shortcut {
    margin-left: auto;
    color: var(--muted);
    font-size: 0.75rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    line-height: 1rem;
  }

  .command-item-disabled {
    color: var(--muted);
  }

  .command-separator {
    height: 1px;
    margin: 0.25rem 0;
    background: color-mix(in srgb, var(--border) 70%, transparent);
  }

  .command-empty {
    padding: 1rem 0.75rem;
    text-align: center;
    color: var(--muted);
    font-size: 0.875rem;
    line-height: 1.25rem;
  }

  .command-dialog-trigger {
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    padding: 0.5rem 1rem;
    font-size: 0.875rem;
    font-weight: 500;
    line-height: 1.25rem;
    transition: opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1);
  }

  .command-dialog-trigger:hover { opacity: 0.9; }

  .command-dialog-overlay {
    position: fixed;
    inset: 0;
    background: rgb(0 0 0 / 0.45);
    opacity: 0;
    transition: opacity 0.2s ease;
    z-index: 50;
  }

  .command-dialog-overlay[data-open] {
    opacity: 1;
  }

  .command-dialog-overlay[data-starting-style],
  .command-dialog-overlay[data-ending-style] {
    opacity: 0;
  }

  .command-dialog-panel {
    position: fixed;
    top: 16.666667%;
    left: 50%;
    transform: translateX(-50%);
    width: 91.666667%;
    padding: 1rem;
    border: 1px solid var(--border);
    background: var(--code-bg);
    box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
    opacity: 0;
    transition:
      opacity 0.2s ease,
      transform 0.2s ease;
    z-index: 60;
    max-width: 36rem;
  }

  .command-dialog-panel[data-open] {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }

  .command-dialog-panel[data-starting-style],
  .command-dialog-panel[data-ending-style] {
    opacity: 0;
    transform: translateX(-50%) translateY(0.5rem);
  }

  .command-dialog-title {
    margin: 0 0 0.25rem;
    font-size: 1rem;
    font-weight: 700;
    color: var(--text);
    line-height: 1.5rem;
  }

  .command-dialog-description {
    margin: 0 0 1rem;
    color: var(--muted);
    font-size: 0.875rem;
    line-height: 1.25rem;
  }
  .command-item-disabled .command-shortcut { color: inherit; }
</style>
<label for="command-extra-command-inline" class="mb-2 block text-xs font-semibold uppercase tracking-wider text-[var(--muted)]">
  Workspace
</label>
<div
  data-slot="command" data-loop
  data-label="Workspace Command Menu"
  class="w-full max-w-md border border-[var(--border)] bg-linear-to-b from-[var(--bg)] to-[var(--code-bg)] shadow-xl"
>
  <div
    data-slot="command-input-wrapper"
    class="flex items-center gap-2 border-b border-[var(--border)]/60 px-4 py-3"
  >
    <span class="text-sm text-[var(--muted)]">⌕</span>
    <input
      data-slot="command-input"
      id="command-extra-command-inline"
      class="w-full border-none bg-transparent text-base text-[var(--text)] outline-none placeholder:text-[var(--muted)]"
      placeholder="Search commands..."
    />
  </div>
  <div data-slot="command-list" class="max-h-72 overflow-auto p-1.5">
    <div data-slot="command-empty" class="px-3 py-4 text-center text-sm text-[var(--muted)]" hidden>No results.</div>
    <div data-slot="command-group">
      <div data-slot="command-group-heading" class="px-3 py-2 text-xs font-bold uppercase tracking-wider text-[var(--muted)]">
        Project
      </div>
      <div
        data-slot="command-item"
        class="flex cursor-default items-center gap-3 px-3 py-2.5 text-sm text-[var(--text)] data-selected:bg-[var(--code-bg)]"
      >
        Open Dashboard
        <span data-slot="command-shortcut" class="ml-auto text-xs uppercase tracking-wider text-[var(--muted)]">↵</span>
      </div>
      <div
        data-slot="command-item"
        data-keywords="invite,team"
        class="flex cursor-default items-center gap-3 px-3 py-2.5 text-sm text-[var(--text)] data-selected:bg-[var(--code-bg)]"
      >
        Invite Teammate
        <span data-slot="command-shortcut" class="ml-auto text-xs uppercase tracking-wider text-[var(--muted)]">⌘I</span>
      </div>
    </div>
    <div data-slot="command-separator" class="my-1 h-px bg-[var(--border)]/70"></div>
    <div data-slot="command-group">
      <div data-slot="command-group-heading" class="px-3 py-2 text-xs font-bold uppercase tracking-wider text-[var(--muted)]">
        Preferences
      </div>
      <div
        data-slot="command-item"
        data-value="settings"
        class="flex cursor-default items-center gap-3 px-3 py-2.5 text-sm text-[var(--text)] data-selected:bg-[var(--code-bg)]"
      >
        Open Settings
        <span data-slot="command-shortcut" class="ml-auto text-xs uppercase tracking-wider text-[var(--muted)]">⌘,</span>
      </div>
      <div
        data-slot="command-item"
        data-disabled
        class="flex cursor-default items-center gap-3 px-3 py-2.5 text-sm text-[var(--muted)]"
      >
        Billing Portal
        <span data-slot="command-shortcut" class="ml-auto text-xs uppercase tracking-wider text-[var(--muted)]">Soon</span>
      </div>
    </div>
  </div>
</div>
Preview

Compose @data-slot/command insidedialog-content to build modal palettes with the same strict cmdk-style focus behavior.

Show codeHide code
<div data-slot="dialog" class="command-dialog">
  <button data-slot="dialog-trigger" class="command-dialog-trigger">Open Command Palette</button>
  <div data-slot="dialog-overlay" class="command-dialog-overlay" hidden></div>
  <div data-slot="dialog-content" class="command-dialog-panel" hidden>
    <h2 data-slot="dialog-title" class="command-dialog-title">Command Palette</h2>
    <p data-slot="dialog-description" class="command-dialog-description">
      Search for a command to run across your workspace.
    </p>

    <div data-slot="command" data-loop data-label="Global Command Palette" class="command-root command-root-dialog">
      <div data-slot="command-input-wrapper" class="command-input-shell">
        <span class="command-search-icon">⌕</span>
        <input data-slot="command-input" class="command-input" placeholder="Search commands..." />
      </div>
      <div data-slot="command-list" class="command-list">
        <div data-slot="command-empty" class="command-empty" hidden>No results.</div>
        <div data-slot="command-item" class="command-item">
          New Issue
          <span data-slot="command-shortcut" class="command-shortcut">⌘N</span>
        </div>
        <div data-slot="command-item" class="command-item">
          Jump to Inbox
          <span data-slot="command-shortcut" class="command-shortcut">G I</span>
        </div>
        <div data-slot="command-item" class="command-item">
          Open Settings
          <span data-slot="command-shortcut" class="command-shortcut">⌘,</span>
        </div>
      </div>
    </div>
  </div>
</div>

<style>
  .command-root {
    width: 100%;
    border: 1px solid var(--border);
    background: linear-gradient(in oklab, var(--bg), var(--code-bg));
    box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
    max-width: 28rem;
  }

  .command-root-dialog {
    width: 100%;
    max-width: none;
    overflow: hidden;
    box-shadow: none;
  }

  .command-field-label {
    display: block;
    margin-bottom: 0.5rem;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--muted);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    line-height: 1rem;
  }

  .command-input-shell {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.75rem 1rem;
    border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
  }

  .command-search-icon {
    color: var(--muted);
    font-size: 0.875rem;
    line-height: 1.25rem;
  }

  .command-input {
    width: 100%;
    border: none;
    background: transparent;
    font-size: 1rem;
    color: var(--text);
    outline: none;
    line-height: 1.5rem;
  }

  .command-input::placeholder {
    color: var(--muted);
  }

  .command-list {
    max-height: 18rem;
    overflow: auto;
    padding: 0.375rem;
  }

  .command-heading {
    padding: 0.5rem 0.75rem;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--muted);
    line-height: 1rem;
  }

  .command-item {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.625rem 0.75rem;
    color: var(--text);
    cursor: default;
    font-size: 0.875rem;
    line-height: 1.25rem;
  }

  .command-item[data-selected] {
    background: var(--code-bg);
  }

  .command-shortcut {
    margin-left: auto;
    color: var(--muted);
    font-size: 0.75rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    line-height: 1rem;
  }

  .command-item-disabled {
    color: var(--muted);
  }

  .command-separator {
    height: 1px;
    margin: 0.25rem 0;
    background: color-mix(in srgb, var(--border) 70%, transparent);
  }

  .command-empty {
    padding: 1rem 0.75rem;
    text-align: center;
    color: var(--muted);
    font-size: 0.875rem;
    line-height: 1.25rem;
  }

  .command-dialog-trigger {
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    padding: 0.5rem 1rem;
    font-size: 0.875rem;
    font-weight: 500;
    line-height: 1.25rem;
    transition: opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1);
  }

  .command-dialog-trigger:hover { opacity: 0.9; }

  .command-dialog-overlay {
    position: fixed;
    inset: 0;
    background: rgb(0 0 0 / 0.45);
    opacity: 0;
    transition: opacity 0.2s ease;
    z-index: 50;
  }

  .command-dialog-overlay[data-open] {
    opacity: 1;
  }

  .command-dialog-overlay[data-starting-style],
  .command-dialog-overlay[data-ending-style] {
    opacity: 0;
  }

  .command-dialog-panel {
    position: fixed;
    top: 16.666667%;
    left: 50%;
    transform: translateX(-50%);
    width: 91.666667%;
    padding: 1rem;
    border: 1px solid var(--border);
    background: var(--code-bg);
    box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
    opacity: 0;
    transition:
      opacity 0.2s ease,
      transform 0.2s ease;
    z-index: 60;
    max-width: 36rem;
  }

  .command-dialog-panel[data-open] {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }

  .command-dialog-panel[data-starting-style],
  .command-dialog-panel[data-ending-style] {
    opacity: 0;
    transform: translateX(-50%) translateY(0.5rem);
  }

  .command-dialog-title {
    margin: 0 0 0.25rem;
    font-size: 1rem;
    font-weight: 700;
    color: var(--text);
    line-height: 1.5rem;
  }

  .command-dialog-description {
    margin: 0 0 1rem;
    color: var(--muted);
    font-size: 0.875rem;
    line-height: 1.25rem;
  }
  .command-item-disabled .command-shortcut { color: inherit; }
</style>
<div data-slot="dialog">
  <button
    data-slot="dialog-trigger"
    class="border border-[var(--border)] bg-[var(--surface)] px-4 py-2 text-sm font-medium text-text transition hover:opacity-90"
  >
    Open Command Palette
  </button>
  <div
    data-slot="dialog-overlay"
    class="fixed inset-0 z-50 bg-black/45 opacity-0 transition-opacity duration-200 data-open:opacity-100 data-starting-style:opacity-0 data-ending-style:opacity-0"
    hidden
  ></div>
  <div
    data-slot="dialog-content"
    class="fixed left-1/2 top-1/6 z-60 w-11/12 max-w-xl -translate-x-1/2 border border-[var(--border)] bg-[var(--code-bg)] p-4 opacity-0 shadow-2xl transition-all duration-200 data-open:opacity-100 data-starting-style:translate-y-2 data-starting-style:opacity-0 data-ending-style:translate-y-2 data-ending-style:opacity-0"
    hidden
  >
    <h2 data-slot="dialog-title" class="mb-1 text-base font-bold text-[var(--text)]">Command Palette</h2>
    <p data-slot="dialog-description" class="mb-4 text-sm text-[var(--muted)]">Search for a command to run across your workspace.</p>

    <div
      data-slot="command" data-loop
      data-label="Global Command Palette"
      class="overflow-hidden border border-[var(--border)] bg-linear-to-b from-[var(--bg)] to-[var(--code-bg)]"
    >
      <div data-slot="command-input-wrapper" class="flex items-center gap-2 border-b border-[var(--border)]/60 px-4 py-3">
        <span class="text-sm text-[var(--muted)]">⌕</span>
        <input
          data-slot="command-input"
          class="w-full border-none bg-transparent text-base text-[var(--text)] outline-none placeholder:text-[var(--muted)]"
          placeholder="Search commands..."
        />
      </div>
      <div data-slot="command-list" class="max-h-72 overflow-auto p-1.5">
        <div data-slot="command-empty" class="px-3 py-4 text-center text-sm text-[var(--muted)]" hidden>No results.</div>
        <div
          data-slot="command-item"
          class="flex cursor-default items-center gap-3 px-3 py-2.5 text-sm text-[var(--text)] data-selected:bg-[var(--code-bg)]"
        >
          New Issue
          <span data-slot="command-shortcut" class="ml-auto text-xs uppercase tracking-wider text-[var(--muted)]">⌘N</span>
        </div>
        <div
          data-slot="command-item"
          class="flex cursor-default items-center gap-3 px-3 py-2.5 text-sm text-[var(--text)] data-selected:bg-[var(--code-bg)]"
        >
          Jump to Inbox
          <span data-slot="command-shortcut" class="ml-auto text-xs uppercase tracking-wider text-[var(--muted)]">G I</span>
        </div>
        <div
          data-slot="command-item"
          class="flex cursor-default items-center gap-3 px-3 py-2.5 text-sm text-[var(--text)] data-selected:bg-[var(--code-bg)]"
        >
          Open Settings
          <span data-slot="command-shortcut" class="ml-auto text-xs uppercase tracking-wider text-[var(--muted)]">⌘,</span>
        </div>
      </div>
    </div>
  </div>
</div>

API reference

Initialization

create(scope?)

Auto-discover and bind all command palettes in a scope (defaults to document).

import { create } from "@data-slot/command";

const controllers = create();

createCommand(root, options?)

Create a controller for a specific element.

import { createCommand } from "@data-slot/command";

const command = createCommand(element, {
  defaultSearch: "set",
  onSelect: (value) => console.log("Selected:", value),
});

Slots

Runtime Slots

  • command - Root container.
  • command-input - Required search input that filters and ranks the commands.
  • command-list - Required listbox container for items and groups.
  • command-empty - Empty state shown when there are no ranked matches.
  • command-group - Group of related items.
  • command-group-heading - Optional label for a group.
  • command-item - Selectable command item.
  • command-shortcut - Optional shortcut hint inside an item; excluded when the runtime infers the item value from text. Displaying it does not register a keyboard shortcut.
  • command-separator - Divider between sections; hidden during search unless data-always-render is enabled.

Style-only Slots

  • command-input-wrapper - Optional wrapper around the input for styling.

Dialog Composition

Use @data-slot/dialog when you want modal presentation:

<div data-slot="dialog">
  <button data-slot="dialog-trigger">Open Command Palette</button>
  <div data-slot="dialog-overlay" hidden></div>
  <div data-slot="dialog-content" hidden>
    <h2 data-slot="dialog-title">Command Palette</h2>
    <p data-slot="dialog-description">Search for a command to run.</p>

    <div data-slot="command" data-label="Global Command Palette">
      <input data-slot="command-input" placeholder="Search commands..." />
      <div data-slot="command-list">
        <div data-slot="command-empty" hidden>No results.</div>
        <div data-slot="command-item">New Issue</div>
        <div data-slot="command-item">Open Settings</div>
      </div>
    </div>
  </div>
</div>

<script type="module">
  import { createDialog } from "@data-slot/dialog";
  import { createCommand } from "@data-slot/command";

  document.querySelectorAll('[data-slot="dialog"]').forEach((el) => createDialog(el));
  document.querySelectorAll('[data-slot="command"]').forEach((el) => createCommand(el));
</script>

Data Attributes

Attribute Applies To Description
data-value command-item, command-group Explicit value. If omitted on an item, value is inferred from data-label or text content
data-label command-item Alternate text source for value inference
data-keywords command-item Comma-separated aliases used during filtering
data-disabled / disabled command-item Prevents navigation and selection
data-force-mount command-item, command-group Keeps the node rendered during filtering
data-always-render command-separator Keeps the separator visible while searching

command-shortcut text is ignored when inferring an item value, so shadcn-style shortcut hints do not affect search matches.

Options

Options can be passed via JavaScript or data attributes on the root element. JavaScript options take precedence.

Option Data Attribute Type Default Description
label data-label string "Command Menu" Accessible label announced for the search input
defaultValue data-default-value string First enabled visible item, or null Initial active item value; a nonempty defaultSearch selects the first enabled result instead
defaultSearch data-default-search string "" Initial search text
shouldFilter data-should-filter boolean true Enable built-in filtering and sorting; set false to manage results yourself
loop data-loop boolean false Wrap arrow-key navigation
disablePointerSelection data-disable-pointer-selection boolean false Disable hover-driven selection
vimBindings data-vim-bindings boolean true Enable Ctrl+J/K/N/P shortcuts
filter - (value, search, keywords?) => number commandScore Custom ranking function
onValueChange - (value: string | null) => void - Called when the active item changes
onSearchChange - (search: string) => void - Called when the search query changes
onSelect - (value: string) => void - Called on click or Enter selection

Controller

interface CommandController {
  readonly value: string | null;
  readonly search: string;
  select(value: string | null): void;
  setSearch(search: string): void;
  destroy(): void;
}

Events

Outbound Events

root.addEventListener("command:change", (event) => {
  console.log("Active item:", event.detail.value);
});

root.addEventListener("command:search-change", (event) => {
  console.log("Search:", event.detail.search);
});

root.addEventListener("command:select", (event) => {
  console.log("Selected:", event.detail.value);
});

Inbound Events

root.dispatchEvent(
  new CustomEvent("command:set", {
    detail: {
      search: "set",
      value: "settings",
    },
  })
);

Styling

[data-slot="command-item"][data-selected] {
  background: #f1f1f1;
}

[data-slot="command-item"][aria-disabled="true"] {
  opacity: 0.5;
  pointer-events: none;
}

[data-slot="command-list"] {
  height: var(--command-list-height);
  transition: height 150ms ease;
}

Keyboard Navigation

Key Action
ArrowDown / ArrowUp Move to the next or previous enabled item
Home / End Jump to the first or last enabled item
Alt+ArrowDown / Alt+ArrowUp Jump between groups
Ctrl+J / Ctrl+N Next item
Ctrl+K / Ctrl+P Previous item
Enter Trigger command:select for the active item

Arrow navigation is handled on the command root like cmdk. Selecting from the input or a focused command root keeps keyboard flow intact, but clicking non-interactive palette chrome does not auto-focus the input.