Vanilla UI Kit Command Palette v1.0.0 · toast →

Every action, one keystroke.

A zero-dependency Ctrl/Cmd+K palette with fuzzy search, groups, shortcuts, and full keyboard control. Try it — everything on this page is wired up.

Nothing run yet — try “theme”, “toast”, or just “sf”.

Register & go

One static call; the palette, hotkey, and styles bootstrap themselves lazily.

CommandPalette.register([
  { id: 'save', label: 'Save file',
    hint: 'Ctrl+S', group: 'Actions',
    action: function () { save() } }
])   // Ctrl/Cmd+K just works

Fuzzy matching

Case- and diacritic-insensitive subsequence match with word-start and consecutive-run bonuses. Matched letters are highlighted.

Open the palette and type sf → Save file, or resume → Résumé template.

{ id: 'docs', label: 'Open documentation',
  keywords: 'help manual guide' }

Theme commands

These commands actually flip this page's theme — the palette (and the whole family) follows <html data-theme> live.

{ id: 'theme-dark', label: 'Theme: Dark',
  group: 'Theme',
  action: function () { setTheme('dark') } }

Recent commands

With recent: true, the last five run commands surface in a “Recent” group — in memory only, per page load.

CommandPalette.defaults.recent = true

Live registry

Register and unregister at any time; re-registering an id replaces it in place. Disabled commands render grayed out.

CommandPalette.register({ id: 'secret', … })
CommandPalette.unregister('secret')

Quick start

One file from the CDN. Keyboard: ↑↓ navigate, ↵ run, esc clears the query, then closes.

<script src="https://cdn.jsdelivr.net/gh/
  vanilla-ui-kit/components/command/command.js">
</script>

API reference

The complete surface of CommandPalette (v1.0.0), as implemented in command.js. Zero dependencies, CommonJS/AMD/global builds, SSR-safe (every method is a no-op without a DOM).

Constructor & usage

var palette = new CommandPalette(options)   // options object, optional — every key optional

The constructor stores options (merged over CommandPalette.defaults), registers any commands passed in, and binds the global hotkey immediately (unless hotkey: false). The overlay DOM and stylesheet are only built and injected on the first open().

Three ways to use it:

Command object shape

Passed to register() (single object or array). Only id is required — entries without an id are silently skipped. Re-registering an existing id replaces the old command in place (list order stays stable).

KeyTypeMeaning
idstring (required) Unique key. Coerced to string; re-registering it replaces the command in place.
labelstring Visible row text (searched and highlighted). Defaults to id.
hintstring Right-aligned shortcut text, e.g. 'Ctrl+S'. Display only — no key binding is created.
groupstring Section header the command is listed under in the empty-query view. Ungrouped commands lead, with no header.
iconstring Trusted inline SVG markup, injected via innerHTML. Never pass untrusted strings.
keywordsstring[] or string Extra match terms; a string is split on whitespace. Not displayed, never highlighted.
actionfunction action(command, palette) — runs on Enter/click, after the palette has closed.
disabledboolean Rendered grayed out with aria-disabled="true"; skipped by keyboard navigation, not runnable.

The palette closes before calling action, so the action can open a modal or move focus without fighting the palette. With recent: true the last 5 run commands appear in a “Recent” group at the top of the empty-query view — in memory only, per page load; nothing is stored.

Matching

Case- and diacritic-insensitive subsequence match over label + keywords (spaces in the query are ignored). Scoring favors consecutive runs, word-starts (so sf finds Save File), and matches near the start of the label; keyword hits rank a shade below equal label hits and are never highlighted. Results are sorted by score (registration order breaks ties) and capped at maxResults. With an empty query all commands are shown, grouped in first-registration order.

Options

Every key of CommandPalette.defaults, plus the constructor-only commands key. Options are shallow-copied over the defaults (labels is merged one level deep); keys set to undefined are ignored. Mutate CommandPalette.defaults before the first static call to configure the default palette.

NameTypeDefaultDescription
commandsobject | object[]undefined Initial commands, passed straight to register() during construction. Constructor-only — not a key of defaults.
hotkeystring | false'mod+k' Global toggle binding, bound on window. 'mod' = Cmd on macOS, Ctrl everywhere else. false (also null/'') = no global binding; open programmatically. See hotkey syntax below.
placeholderstring'Type a command…' Placeholder text of the search input.
maxResultsnumber12 Cap on filtered results (a non-numeric value falls back to 12). An empty query shows all commands, uncapped.
emptyTextstring'No matching commands' Message shown when nothing matches the query.
recentbooleanfalse true = show a session-only “Recent” group (last 5 run commands) at the top of the empty-query view. In memory only — no storage.
stylesbooleantrue false = headless: no CSS is ever injected; you style the .vcmd-* markup yourself (see Theming).
themestring'auto' 'auto' | 'light' | 'dark'. Auto follows <html data-theme> / data-bs-theme / .dark/.light class, then prefers-color-scheme, re-resolved live.
labelsobjectsee below UI / ARIA strings, merged key-by-key with the defaults — override any subset (e.g. for localization).
onOpenfunction | nullnull fn(palette) — called after the palette opens. See Events & callbacks.
onClosefunction | nullnull fn(palette) — called when the palette starts closing.
onRunfunction | nullnull fn(command, palette) — called after a command runs (after its action).

The labels keys

KeyDefaultUsed for
title'Command palette'aria-label of the dialog panel.
search'Search commands'aria-label of the search input.
commands'Commands'aria-label of the results listbox.
recent'Recent'Header of the recent-commands group.
navigate'navigate'Footer legend next to ↑↓.
run'run'Footer legend next to ↵.
close'close'Footer legend next to esc.

Instance methods

All methods return the instance, so calls chain. Readable instance properties: isOpen (boolean) and commands (the normalized command array).

MethodDescription
register(command | commands[]) Append command(s). A command whose id is already registered replaces the old one in place (order stays stable). Re-renders live if the palette is open.
unregister(id) Remove the command with that id (and drop it from the recent list). Re-renders live if open.
open() Open the palette: injects styles (unless styles: false), builds the DOM on first use, clears the query, focuses the input, then calls onOpen. No-op if already open (or without a DOM).
close() Close the palette and return focus to the element focused before opening; calls onClose. No-op if already closed.
toggle() open() if closed, close() if open.
destroy() Unbind the global hotkey, remove the overlay from the DOM, and drop the instance from the theme watcher.

Statics & helpers

CommandPalette.register / unregister / open / close / toggle proxy a lazily created default instance — nothing is built or bound until the first call — and return CommandPalette for chaining.

StaticDescription
CommandPalette.register(cmd | cmds[]) Register on the default palette (creating it on first call).
CommandPalette.unregister(id) Unregister from the default palette.
CommandPalette.open() / .close() / .toggle() Open / close / toggle the default palette.
CommandPalette.autoInit(root) Wire every [data-command-open] element under root (default: document) to open the default palette on click. Idempotent — already-wired elements are skipped. Returns the array of newly wired elements. Runs automatically on DOM ready.
CommandPalette.defaults The shared defaults object (see Options). Mutate before the default palette is created to configure the zero-setup surface.
CommandPalette.salt CSS isolation token, default 'vc1'. Set your own token or false (no salting) before the first open. See Theming.
CommandPalette.css The full embedded stylesheet as a string, rendered with the current salt (a live getter where supported). Also shipped as dist/command.css.
CommandPalette.version Version string, '1.0.0'.
displayName, rootClass, themeVars, varScopes Convergence contract for the optional VC family core ('CommandPalette', 'vcmd', the accent/radius/font variable map, and the scopes VC.config() writes to). Informational — not needed in normal use.

Events & callbacks

The palette dispatches no DOM events — all notification is via option callbacks and the per-command action:

CallbackSignatureWhen
onOpenfn(palette) After the palette opens and the input is focused.
onClosefn(palette) When closing begins (focus has already been restored; the exit animation may still be running).
onRunfn(command, palette) After a command is run via Enter or click — after the palette closes and after the command's own action.
command.actionaction(command, palette) The command's own handler. Runs after the palette closes, before onRun.

In all callbacks, command is the normalized command object ({ id, label, hint, group, icon, keywords[], action, disabled }) and palette is the instance the command ran on.

Declarative init

There is exactly one declarative hook: any element with a data-command-open attribute opens the default palette on click. Wiring happens automatically on DOMContentLoaded (or immediately if the script loads after DOM ready); for content added later, call CommandPalette.autoInit(root) — elements are only wired once. Commands themselves cannot be declared via data-* attributes; registration is JavaScript-only.

<button type="button" data-command-open>Open command palette</button>

<script>
  // after injecting new markup:
  CommandPalette.autoInit(document.getElementById('late-content'))
</script>

Theming

Auto light/dark with the family's resolution order: <html data-theme> / data-bs-theme / .dark/.light class → prefers-color-scheme, re-resolved live (via the VC core's watcher when loaded, otherwise a private MutationObserver + media-query listener). Pin it with CommandPalette.defaults.theme = 'dark'. All colors are CSS custom properties on the .vcmd root:

PropertyLight defaultDark defaultPurpose
--vcmd-accent#5b5bd6#7b7bea Match-highlight marks, active icon, focus rings.
--vcmd-bg#ffffff#1b1d24 Panel background.
--vcmd-surface#f2f2f5#272a33 Active row background, footer key caps.
--vcmd-text#1c1d21#e9eaf0 Primary text.
--vcmd-muted#72747e#989aa6 Placeholder, hints, group headers, icons, footer.
--vcmd-faint#e7e7ec#31343f Borders and separators.
--vcmd-shadow0 24px 64px rgba(24,25,32,.22), 0 4px 16px rgba(24,25,32,.1) 0 24px 64px rgba(0,0,0,.55), 0 4px 16px rgba(0,0,0,.4) Panel drop shadow.
--vcmd-backdroprgba(24,25,32,.42)rgba(0,0,0,.55) Full-screen backdrop color.
--vcmd-radius14px— Panel corner radius.
--vcmd-fontsystem-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif— Font family for the whole palette.
.vcmd { --vcmd-accent: #b45309; --vcmd-radius: 10px; }   /* your overrides */

With the VC core loaded, VC.config({ accent: '#b45309' }) themes the palette and every other family component in one call.

CSS isolation: the overlay renders as class="vcmd vc1" and all structural rules ship salted (.vcmd.vc1 .vcmd-option { … }) so host-page design systems can't override the palette — while the --vcmd-* variable overrides above keep working (variable definitions are deliberately unsalted). Custom token: CommandPalette.salt = 'acme' before the first open; disable with CommandPalette.salt = false.

Headless: set CommandPalette.defaults.styles = false (or per instance styles: false) and no CSS is ever injected. You keep the full behavior — fuzzy matching, keyboard model, ARIA, recent group — and style the markup contract below entirely from your own CSS. The stock stylesheet is available as a starting point via CommandPalette.css (string) or dist/command.css (file).

.vcmd[data-theme="dark"].vcmd-open      ← .vcmd-out while leaving
  .vcmd-backdrop
  .vcmd-panel                           ← role=dialog
    .vcmd-search
      .vcmd-glass                       ← magnifier SVG
      .vcmd-input                       ← role=combobox
    .vcmd-list                          ← role=listbox
      .vcmd-group                       ← section header (role=presentation)
      .vcmd-option.is-active            ← role=option; .is-disabled
        .vcmd-icon                      ← only when `icon` given
        .vcmd-label                     ← .vcmd-mark spans wrap matches
        .vcmd-hint                      ← only when `hint` given
      .vcmd-empty                       ← only when nothing matches
    .vcmd-footer                        ← ↑↓ navigate · ↵ run · esc close

Accessibility & keyboard

KeyAction
Ctrl/⌘+K (configurable) Toggle the palette. Bound on window; deliberately fires even while an input, textarea, or contenteditable has focus — it is the palette's only global binding, so ordinary typing is never intercepted.
↑ / ↓ Move the active command. Wraps in both directions, skips disabled commands, scrolls the row into view.
Enter Run the active command (ignored mid-IME-composition). The palette closes first, then action, then onRun.
Esc Clears the query first if non-empty; a second Esc closes the palette.
Tab Trapped — focus stays in the search input (combobox pattern).

Hotkey syntax

Simple modifier+…+key strings: 'mod+k' (default), 'ctrl+shift+p', 'alt+space'. mod means Cmd on macOS, Ctrl everywhere else; other modifier aliases are ctrl/control, shift, alt/option, and meta/cmd/command/win. Set hotkey: false to disable the global binding.

ARIA


Headless mode

styles: false injects nothing — fuzzy matching, the keyboard model, ARIA and the stable .vcmd-* class hooks all remain; you bring your own CSS. The demos above have already injected the kit stylesheet into this page, so a headless instance here would still match those rules — each demo below therefore runs inside its own <iframe>, a genuinely clean document.

Unstyled (styles: false)

Raw browser rendering, zero CSS — open it: fuzzy search, ↑↓, Enter, Esc and the match highlighting all still work.

var pal = new CommandPalette({
  styles: false,   // no CSS injected
  commands: [ /* … */ ]
})
openBtn.onclick = function () { pal.open() }

Bring your own design (Tailwind)

The same hooks mapped to a dark launcher look with Tailwind @apply. Loads cdn.tailwindcss.com only when you ask.

.vcmd { @apply fixed inset-0 z-50 flex items-start
  justify-center p-4 pt-[10vh] font-sans; }
.vcmd-backdrop { @apply absolute inset-0
  bg-slate-950/70 backdrop-blur-sm; }
.vcmd-panel { @apply relative z-10 w-full max-w-lg
  overflow-hidden rounded-2xl border border-white/10
  bg-slate-900 text-slate-200 shadow-2xl; }
.vcmd-search { @apply flex items-center gap-3
  border-b border-white/10 px-4 py-3; }
.vcmd-input { @apply w-full border-0 bg-transparent
  text-base text-slate-100 outline-none; }
.vcmd-list { @apply max-h-72 overflow-y-auto p-2; }
.vcmd-group { @apply px-3 pt-3 pb-1 text-[11px]
  font-semibold uppercase tracking-widest text-slate-500; }
.vcmd-option { @apply flex cursor-pointer items-center
  gap-3 rounded-lg px-3 py-2.5 text-sm; }
.vcmd-option.is-active { @apply bg-amber-400/10 text-amber-300; }
.vcmd-option.is-disabled { @apply opacity-40 cursor-not-allowed; }
.vcmd-mark { @apply rounded-sm bg-amber-400/20 text-amber-200; }
.vcmd-hint { @apply text-xs text-slate-500; }
.vcmd-empty { @apply px-3 py-10 text-center text-slate-500; }
.vcmd-footer { @apply flex gap-4 border-t border-white/10
  px-4 py-2 text-[11px] text-slate-500; }
.vcmd-key { @apply rounded border border-white/15 bg-white/5
  px-1.5 py-0.5 font-mono text-[10px] text-slate-400; }