Accessibility pattern · Buttons and actions

Command palette

The palette is a modal dialog around a combobox: typing filters a grouped listbox, and the arrow keys move a highlight while focus stays in the field. aria-activedescendant tells a screen reader which command is highlighted, and a polite status says how many match once typing pauses.

WCAG criteria
7
Keyboard rules
7
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Notebook

The shortcut works while focus is anywhere in this demo: Ctrl+K, or ⌘+K on a Mac.

No command run yet

New note
Duplicate note
Export as PDF
Inbox
Calendar
Settings
Switch theme
Zoom in
Keyboard shortcuts

9 commands

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
CtrlK or ⌘KOpens the palette while focus is anywhere in the demo; pressed in the palette, selects the search text.
Enter or SpaceOn Search commands, opens the palette with focus in the search field.
Arrow Down or Arrow UpMoves the highlight to the next or previous matching command, wrapping at the ends and skipping group names.
A–ZTyping filters the commands; the number that match is announced when typing pauses.
Ctrl, or CtrlShiftL or Ctrl/With the palette closed and focus in the demo, runs the command whose hint shows those keys (⌘ instead of Ctrl on a Mac).
EnterRuns the highlighted command, closes the palette and returns focus to where it was before.
EscapeCloses the palette without running anything and returns focus.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Focus reaches the buttonSearch commands, button, opens a dialog, Control+K
The palette opensCommand palette, dialog. Search commands, combo box, expanded. New note, 1 of 9
Arrow Down moves the highlightDuplicate note, 2 of 9
Typing "cal" leaves one match1 command
Nothing matchesNo commands match
Enter runs CalendarSearch commands, button. Ran: Calendar

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-command-palette" data-ap-command-palette>
  <div class="ap-command-palette__app">
    <div class="ap-command-palette__bar">
      <p class="ap-command-palette__brand">
        <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 3h9l4 4v14H6Z"/><path d="M14 3v5h5"/><path d="M9 13h6M9 17h4"/></svg>
        <span>Notebook</span>
      </p>
      <button type="button" class="ap-command-palette__open" data-ap-open aria-haspopup="dialog" aria-keyshortcuts="Control+K Meta+K">
        <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
        <span class="ap-command-palette__open-text">Search commands</span>
        <span class="ap-command-palette__keys" aria-hidden="true"><kbd>Ctrl</kbd><kbd>K</kbd></span>
      </button>
    </div>
    <div class="ap-command-palette__page">
      <label class="ap-command-palette__note-label" for="command-palette-note">Quick note</label>
      <textarea class="ap-command-palette__note" id="command-palette-note" rows="3" placeholder="Write something, then press Ctrl+K without leaving the field."></textarea>
      <p class="ap-command-palette__hint">The shortcut works while focus is anywhere in this demo: <span class="ap-command-palette__combo"><kbd>Ctrl</kbd>+<kbd>K</kbd></span>, or <span class="ap-command-palette__combo"><kbd>⌘</kbd>+<kbd>K</kbd></span> on a Mac.</p>
    </div>
    <p class="ap-command-palette__status" role="status">No command run yet</p>
  </div>

  <dialog class="ap-command-palette__dialog" aria-label="Command palette">
    <div class="ap-command-palette__search">
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
      <input class="ap-command-palette__input" id="command-palette-input" type="text" role="combobox" aria-label="Search commands" aria-expanded="true" aria-controls="command-palette-list" aria-autocomplete="list" autocomplete="off" spellcheck="false" placeholder="Type a command or search" />
      <kbd class="ap-command-palette__esc" aria-hidden="true">Esc</kbd>
    </div>
    <div class="ap-command-palette__list" id="command-palette-list" role="listbox" aria-label="Commands">
      <div class="ap-command-palette__group" role="group" aria-labelledby="command-palette-g-notes">
        <div class="ap-command-palette__group-name" id="command-palette-g-notes" role="presentation">Notes</div>
        <div class="ap-command-palette__opt" role="option" id="command-palette-o-new" aria-selected="false" data-words="new note create add page">
          <svg class="ap-command-palette__ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 5v14M5 12h14"/></svg>
          <span class="ap-command-palette__name">New note</span>
        </div>
        <div class="ap-command-palette__opt" role="option" id="command-palette-o-duplicate" aria-selected="false" data-words="duplicate note copy clone">
          <svg class="ap-command-palette__ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg>
          <span class="ap-command-palette__name">Duplicate note</span>
        </div>
        <div class="ap-command-palette__opt" role="option" id="command-palette-o-export" aria-selected="false" data-words="export as pdf download save print">
          <svg class="ap-command-palette__ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 4v11"/><path d="m7 10 5 5 5-5"/><path d="M5 20h14"/></svg>
          <span class="ap-command-palette__name">Export as PDF</span>
        </div>
      </div>
      <div class="ap-command-palette__group" role="group" aria-labelledby="command-palette-g-go">
        <div class="ap-command-palette__group-name" id="command-palette-g-go" role="presentation">Go to</div>
        <div class="ap-command-palette__opt" role="option" id="command-palette-o-inbox" aria-selected="false" data-words="go to inbox messages mail">
          <svg class="ap-command-palette__ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3 13h5l1.5 3h5l1.5-3h5"/><path d="M5.5 5h13L21 13v6H3v-6Z"/></svg>
          <span class="ap-command-palette__name">Inbox</span>
        </div>
        <div class="ap-command-palette__opt" role="option" id="command-palette-o-calendar" aria-selected="false" data-words="go to calendar schedule events dates">
          <svg class="ap-command-palette__ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3.5" y="5" width="17" height="15" rx="2"/><path d="M3.5 10h17M8 3v4M16 3v4"/></svg>
          <span class="ap-command-palette__name">Calendar</span>
        </div>
        <div class="ap-command-palette__opt" role="option" id="command-palette-o-settings" aria-selected="false" aria-keyshortcuts="Control+, Meta+," data-words="go to settings preferences options account">
          <svg class="ap-command-palette__ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="3"/><path d="M12 2.5v3M12 18.5v3M4.2 6.5l2.6 1.5M17.2 16l2.6 1.5M4.2 17.5 6.8 16M17.2 8l2.6-1.5"/></svg>
          <span class="ap-command-palette__name">Settings</span>
          <span class="ap-command-palette__keys" aria-hidden="true"><kbd>Ctrl</kbd><kbd>,</kbd></span>
        </div>
      </div>
      <div class="ap-command-palette__group" role="group" aria-labelledby="command-palette-g-view">
        <div class="ap-command-palette__group-name" id="command-palette-g-view" role="presentation">Display</div>
        <div class="ap-command-palette__opt" role="option" id="command-palette-o-theme" aria-selected="false" aria-keyshortcuts="Control+Shift+L Meta+Shift+L" data-words="switch theme dark light mode appearance">
          <svg class="ap-command-palette__ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5Z"/></svg>
          <span class="ap-command-palette__name">Switch theme</span>
          <span class="ap-command-palette__keys" aria-hidden="true"><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>L</kbd></span>
        </div>
        <div class="ap-command-palette__opt" role="option" id="command-palette-o-zoom" aria-selected="false" data-words="zoom in bigger larger text size">
          <svg class="ap-command-palette__ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5M11 8v6M8 11h6"/></svg>
          <span class="ap-command-palette__name">Zoom in</span>
        </div>
        <div class="ap-command-palette__opt" role="option" id="command-palette-o-keys" aria-selected="false" aria-keyshortcuts="Control+/ Meta+/" data-words="keyboard shortcuts keys hotkeys help">
          <svg class="ap-command-palette__ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="2.5" y="6" width="19" height="12" rx="2"/><path d="M6 10h.01M9.5 10h.01M13 10h.01M16.5 10h.01M7 14h10"/></svg>
          <span class="ap-command-palette__name">Keyboard shortcuts</span>
          <span class="ap-command-palette__keys" aria-hidden="true"><kbd>Ctrl</kbd><kbd>/</kbd></span>
        </div>
      </div>
    </div>
    <div class="ap-command-palette__empty" data-ap-no-match hidden>
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5M8.5 8.5l5 5M13.5 8.5l-5 5"/></svg>
      <p class="ap-command-palette__empty-lead">No commands match</p>
      <p class="ap-command-palette__empty-hint">Try a shorter word, or another name for what you want to do.</p>
    </div>
    <div class="ap-command-palette__foot">
      <p class="ap-command-palette__count"><span data-ap-n translate="no">9</span> <span data-ap-unit>commands</span></p>
      <p class="ap-command-palette__how" aria-hidden="true"><kbd>↑</kbd><kbd>↓</kbd> <span>to move</span> <kbd>↵</kbd> <span>to run</span></p>
    </div>
    <p class="ap-command-palette__say" role="status" data-ap-say></p>
  </dialog>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    Commands sit in named groups inside the listbox, so a screen reader can tell the Go to commands from the Display ones.

  • 1.4.1 Use of Color Level A

    The highlighted command gets a bar along its edge as well as a fill, so it does not rely on color.

  • 2.1.1 Keyboard Level A

    A visible button opens the palette as well as the shortcut, and every command runs from the arrow keys and Enter.

  • 2.1.4 Character Key Shortcuts Level A

    The shortcut needs Ctrl or ⌘, so it cannot fire while someone types or dictates, and it listens only inside the demo.

  • 2.4.3 Focus Order Level A

    Focus goes into the search field when the palette opens and back to the button or field that opened it when it closes.

  • 4.1.2 Name, Role, Value Level A

    The field is a combobox with aria-expanded and aria-controls; aria-activedescendant points at the highlighted option, which has aria-selected.

  • 4.1.3 Status Messages Level AA

    The number of matches is announced from a polite status when typing pauses, without moving focus.

Usage

When to use it

Use it

  • Apps with more commands than fit in menus and toolbars, for people who would rather type than hunt.
  • A fast route to actions that also exist somewhere visible in the interface.

Use something else

  • As the only home of a command: people who never press Ctrl+K need to find it in the interface.
  • Searching a site's content: a search field with a results page is easier to understand and to bookmark.
  • A handful of actions: a menu button or a toolbar shows them without any typing.

Common failures

How it usually goes wrong

  • A shortcut that listens everywhere

    Ctrl+K on the whole document takes the key from editors and other widgets on the page. The listener here sits on the demo, so it answers only inside it.

  • Moving focus into the list

    If the arrow keys move real focus to the options, typing stops until people go back to the field. aria-activedescendant moves only the highlight.

  • Results nobody hears

    Sighted people see the list shrink; screen reader users hear nothing unless the count is announced. A polite status gives it when typing pauses.

  • Announcing every keystroke

    Updating a live region on each letter makes a screen reader talk over the typing. The count waits for a short pause.

  • A floating box instead of a dialog

    A positioned div leaves the page behind open to Tab and the virtual cursor. showModal() makes the rest of the page inert while the palette is open.

  • Shortcut hints that do nothing

    A hint beside a command promises the keys work. Every hint here runs its command, and aria-keyshortcuts gives the same keys to assistive technology.

Notes

Building it

  • Only commands with a shortcut show a hint, and each hint is wired: the visible keys are aria-hidden because aria-keyshortcuts already exposes them.
  • Matching uses the label plus a few hidden keywords per command (data-words), so typing "dark" finds Switch theme.
  • Group names are role=presentation elements that each group's aria-labelledby points to, as in the grouped listbox example in the WAI-ARIA Authoring Practices.
  • When nothing matches, the listbox is hidden, the combobox reports aria-expanded=false, and an empty state says what to try next.
  • The demo stops Ctrl+K from reaching the page once it has handled it, so a site-wide search on the same keys does not open as well.

Sources: WAI-ARIA Authoring Practices: Combobox · WAI-ARIA Authoring Practices: Listbox · HTML: the dialog element

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction