Try It

Press K (Mac) or CtrlK to open the palette, or use the button below. Type to filter, use arrow keys to navigate, and press Enter to select. Try "Pick DOM elements" for a drill-down demo.

Select a command to see events here...

Usage

Define commands as JSON in a script tag, then drop in the component.

<!-- 1. Define your commands -->
<script type="application/json" data-palette>
[
  { "name": "save", "description": "Save document" },
  { "name": "pick-thing", "description": "Pick a thing", "keepOpen": true }
]
</script>

<!-- 2. Add the component -->
<command-palette></command-palette>
<script src="command-palette.wc.js"></script>

<!-- 3. Listen for events -->
<script>
const palette = document.querySelector('command-palette');
palette.addEventListener('save', () => {
  console.log('Save triggered!');
});
</script>

Dynamic Data & Drill-down

Use keepOpen to prevent closing on selection, then setCommands() to swap in new items. Press Backspace on an empty input to go back.

// Command with keepOpen stays open on select
{ "name": "pick-dom", "description": "Pick DOM elements", "keepOpen": true }

// Handle the event: swap in dynamic data
palette.addEventListener('pick-dom', () => {
  const elements = [...document.querySelectorAll('[class]')].map(el => ({
    name: 'element-picked',
    description: `<${el.tagName.toLowerCase()}> .${[...el.classList].join('.')}`
  }));
  palette.setCommands(elements, {
    placeholder: 'Pick an element...'
  });
});

// The palette auto-resets to the original
// commands when closed or dismissed

Dynamic Base Commands

When the root list itself is backed by app state (subscriptions, notes, tabs), use setBaseCommands(). Unlike setCommands(), this replaces the root list — no back button, no reset on close. Pair it with onBeforeOpen for just-in-time refresh.

// Refresh the root list whenever the palette is about to open
palette.onBeforeOpen = () => {
  const commands = [
    { name: 'quick-add', description: 'Add subscription' },
    ...state.subscriptions.map(s => ({
      name: 'edit-sub',
      description: `Edit: ${s.name}`,
      _subId: s.id  // any extra metadata is preserved in e.detail.command
    }))
  ];
  palette.setBaseCommands(commands);
};

// One listener handles every matching command
palette.addEventListener('edit-sub', (e) => {
  const sub = state.subscriptions.find(s => s.id === e.detail.command._subId);
  openEditor(sub);
});

API

Methods, events, and keyboard shortcuts.

// Open / close programmatically
palette.open();
palette.close();

// Replace the root command list (for dynamic data)
palette.setBaseCommands(commands);

// Called just before open() shows the panel
palette.onBeforeOpen = () => { /* refresh commands */ };

// Drill-down (adds back button, resets on close)
palette.setCommands(commands, {
  placeholder: 'Pick something...',  // optional
  label: 'Elements'               // optional aria-label
});

// Each selected command fires a CustomEvent
// where event.type === command.name
palette.addEventListener('theme-toggle', (e) => {
  console.log(e.detail.command);
  // { name: "theme-toggle", description: "Toggle theme" }
});

// Declarative open / close via the Invoker Commands API:
// works with any element that has an id.
<command-palette id="palette"></command-palette>
<button commandfor="palette" command="--open">Open</button>
<button commandfor="palette" command="--toggle">Toggle</button>
// Supported custom commands: --open, --close, --toggle

// Opt in to "search-as-you-type": pressing any printable key on the
// page (when no input/textarea/contenteditable is focused) opens the
// palette pre-filled with that character. "/" opens without populating.
<command-palette open-on-type></command-palette>