Plugin example

A11y Command Menu Button

A compact command menu for application actions that need real buttons, roving focus, disabled states, one-level submenus, optional typeahead, and predictable focus restoration.

Package
a11y-command-menu-button
Pattern
Menu button
Runtime
Vanilla TypeScript
Dependencies
None

Scenario

Compose actions without leaving the field.

A support workspace needs a plus button beside a note field. Agents can upload files, insert saved templates, extract text when attachments are available, and understand unavailable actions before trying to activate them.

  • Open the menu from the plus button with pointer, touch, Enter, Space, or ArrowDown.
  • Move between commands with arrow keys, Home, End, or optional typeahead.
  • Open the Templates submenu with ArrowRight and return with ArrowLeft.
  • Use the state playground to inspect disabled, busy, mobile sheet, and portal modes.

Live demo

State playground and command menu

The controls beside the example only change demo markup and initialization options. They do not add package behavior.

Support note Add context to the customer timeline. The command menu keeps compact actions close to the text field while preserving keyboard access.

Open the menu, use arrow keys to move, type first letters to jump, and press Escape to close.

Choose an action from the command menu.

Keyboard walkthrough

Follow the focus path

Start the walkthrough, then use the command menu with keyboard only. The active step updates from demo events and focus changes.

Start by moving focus to the command menu trigger.

  1. Reach the trigger Tab to the plus button. It has a usable accessible name.

  2. Open the menu Press Enter, Space, ArrowDown, or ArrowUp.

  3. Move between items Use arrows, Home, End, or type the first letters.

  4. Enter and leave submenu Use ArrowRight to open Templates and ArrowLeft to return.

  5. Close and restore focus Press Escape. Focus returns to the opener by default.

Developer notes

Copy the markup and initialization path

Import the package CSS, provide semantic menu markup, and initialize the root element. The demo uses the same public selectors as the README.

Install

npm install a11y-command-menu-button

JavaScript

import { createCommandMenuButton } from "a11y-command-menu-button";
import "a11y-command-menu-button/styles.css";

const root = document.querySelector("[data-command-menu]");

if (root instanceof HTMLElement) {
  createCommandMenuButton(root, {
    hoverSubmenus: true,
    mobileMode: "sheet",
    portal: false,
    typeahead: true
  });
}

Shortcut labels

The package stylesheet includes a shortcut label class for visual hints. The label does not register a global keyboard shortcut; keep that routing in the host app so editable fields, platform conventions, and user settings can be respected.

<button
  class="a11y-command-menu-button__item"
  data-command-item
  data-command-id="add-files"
  type="button"
>
  <span class="a11y-command-menu-button__item-label">Add files</span>
  <span class="a11y-command-menu-button__shortcut" aria-hidden="true">U</span>
</button>

Minimum markup

Use a real button for the trigger and real buttons or links for commands.

<div class="a11y-command-menu-button" data-command-menu>
  <button
    type="button"
    class="a11y-command-menu-button__trigger"
    data-command-trigger
    aria-label="Open command menu"
    aria-controls="command-menu"
  >
    <span aria-hidden="true">+</span>
  </button>

  <div id="command-menu" class="a11y-command-menu-button__panel" data-command-menu-panel hidden>
    <button class="a11y-command-menu-button__item" data-command-item data-command-id="add-files">
      Add files
    </button>
  </div>
</div>
Public options used by this demo
Option Type Demo value Purpose
typeahead boolean true Lets users type the first letters of enabled commands.
hoverSubmenus boolean true Allows pointer hover to open one-level submenus.
mobileMode "sheet" | "menu" "sheet" Uses bottom-sheet placement below the package mobile breakpoint.
portal boolean Playground controlled Moves menu and submenu layers to document.body.

Accessibility behavior

What this example is designed to expose

This demo documents expected behavior from the plugin and sample markup. Test the final integration with your target assistive technologies and browser matrix.

Semantics and focus

  • The trigger is a real <button> with a non-visual plus icon and an accessible name.
  • The plugin sets aria-expanded, aria-controls, role="menu", and role="menuitem".
  • Arrow navigation uses roving tabindex; Tab exits and closes the menu.
  • Escape closes the menu and restores focus to the trigger by default.

States and announcements

  • aria-disabled="true" commands remain discoverable but dispatch a disabled-command event.
  • aria-busy="true" or data-command-loading remains keyboard discoverable but prevents command activation while busy.
  • Enabled link commands retain native navigation; Tab and Shift+Tab close the menu and continue around the trigger.
  • Portal mode preserves the component theme by copying public custom properties to moved layers.
  • A visible polite status region reports selections, disabled reasons, and playground changes.
  • Default CSS respects reduced motion and includes forced-colors support for menu layers.

Command IDs and event privacy

  • data-command-id values can reveal user intent when forwarded to analytics or logs.
  • Use stable, non-sensitive IDs and avoid embedding document names, customer names, or free text.
  • When tracking command events, send grouped or redacted values instead of raw labels or full event payloads.

Limitations

  • The plugin does not generate menu markup; integrations must provide semantic HTML.
  • Only one submenu level is supported.
  • The menu pattern is intended for compact application commands, not long navigation lists.
  • Screen reader output depends on the final labels, roles, states, and browser/AT pairing.

Selectors used here

  • [data-command-menu] for the root.
  • [data-command-trigger] for the trigger button.
  • [data-command-menu-panel] for the menu layer.
  • [data-command-item], [data-command-submenu-trigger], and [data-command-submenu] for commands and submenus.
One-level submenu support and integration boundaries
Pattern Support Runtime or host responsibility
Top-level command opens one submenu Supported The plugin manages focus, aria-expanded, placement, and close behavior.
Submenu nested inside another submenu Unsupported The runtime logs a warning when nested data-command-submenu elements are detected.
Deep command hierarchy Host-owned design decision Flatten commands, split groups, or use a larger command palette/dialog pattern instead.