PixelMenubar

Horizontal application menubar with nested submenus, keyboard navigation, and shortcut hints.

  • Top-level menus with click + hover-to-switch behavior
  • Nested submenus with right-arrow open / left-arrow close
  • WAI-ARIA menubar keyboard model: arrows, Home/End, Enter/Space, Escape and Tab
  • Shortcut labels and disabled / separator items
  • Surface-aware (border, radius, font)
Status
stable
Category
navigation
Since
v1.9.0

API

import { PixelMenubar } from '@pxlkit/ui-kit';
PropTypeDefaultDescription
menus (required)PixelMenubarMenu[]—The menus, in order.
surface'pixel' | 'linear'—Surface override; defaults to the nearest provider.

Also takes the native attributes and event handlers of <div> (HTMLAttributes<HTMLDivElement>).

ref points to <div>.

Accessibility

WCAG target: 2.1 AA

  • menubar
  • menu
  • menuitem

The menubar is one tab stop: the menu button that last had focus or a menu open. Menu buttons are role="menuitem" with aria-haspopup="menu", aria-expanded and aria-controls wired to the open menu, which is role="menu" labelled by its button. The open menu takes focus (tabindex="-1") and points aria-activedescendant at the highlighted item — in the menu or its submenu — so assistive technology follows the arrows and Home/End. Separators (role="separator") and disabled items (aria-disabled) are skipped; items with a submenu carry aria-haspopup and aria-expanded, and the submenu is named after its item. Choosing an item, Escape and Tab return focus to the menu button; a press outside closes every menu and leaves focus where the pointer put it.

Keyboard

KeyActionWhen
Enter / SpaceOpen its menu with focus in it, highlighting the first enabled item.menu button focused
ArrowDownOpen its menu with focus in it, highlighting the first enabled item.menu button focused
ArrowUpOpen its menu with focus in it, highlighting the last enabled item.menu button focused
ArrowRight / ArrowLeftOpen the next / previous menu, wrapping round, with focus in it.menu button focused
ArrowDown / ArrowUpHighlight the next / previous enabled item, wrapping round — in the submenu when the highlight is in one.menu open
Home / EndHighlight the first / last enabled item — of the submenu when the highlight is in one.menu open
ArrowRightOpen the submenu, highlighting its first enabled item.item with a submenu highlighted
ArrowLeftClose the submenu; the highlight returns to its item.submenu open
ArrowRight / ArrowLeftClose the menu and open the next / previous one, wrapping round.menu open
Enter / SpaceChoose the highlighted item, close the menu and return focus to its button — or open the highlighted item’s submenu on its first item.menu open
EscapeClose the submenu; the highlight returns to its item.submenu open
EscapeClose the menu and return focus to its button.menu open
Tab / Shift+TabClose the menu and move focus on from its button, out of the menubar.menu open

Usage

import { PixelMenubar, type PixelMenubarMenu } from '@pxlkit/ui-kit';

const menus: PixelMenubarMenu[] = [
  {
    label: 'File',
    items: [
      { value: 'new', label: 'New', shortcut: 'Ctrl+N' },
      { value: 'open', label: 'Open…', shortcut: 'Ctrl+O' },
      { value: 'sep-1', label: '', separator: true },
      {
        value: 'recent',
        label: 'Open Recent',
        submenu: [
          { value: 'r1', label: 'project-alpha.pxl' },
          { value: 'r2', label: 'project-beta.pxl' },
        ],
      },
      { value: 'sep-2', label: '', separator: true },
      { value: 'save', label: 'Save', shortcut: 'Ctrl+S' },
      { value: 'quit', label: 'Quit', shortcut: 'Ctrl+Q' },
    ],
  },
  {
    label: 'Edit',
    items: [
      { value: 'undo', label: 'Undo', shortcut: 'Ctrl+Z' },
      { value: 'redo', label: 'Redo', shortcut: 'Ctrl+Y', disabled: true },
      { value: 'sep-3', label: '', separator: true },
      { value: 'cut', label: 'Cut', shortcut: 'Ctrl+X' },
      { value: 'copy', label: 'Copy', shortcut: 'Ctrl+C' },
      { value: 'paste', label: 'Paste', shortcut: 'Ctrl+V' },
    ],
  },
  {
    label: 'View',
    items: [
      { value: 'zoom-in', label: 'Zoom In', shortcut: 'Ctrl++' },
      { value: 'zoom-out', label: 'Zoom Out', shortcut: 'Ctrl+-' },
    ],
  },
];

export function Default() {
  return <PixelMenubar menus={menus} />;
}

Examples

Default

import { PixelMenubar, type PixelMenubarMenu } from '@pxlkit/ui-kit';

const menus: PixelMenubarMenu[] = [
  {
    label: 'File',
    items: [
      { value: 'new', label: 'New', shortcut: 'Ctrl+N' },
      { value: 'open', label: 'Open…', shortcut: 'Ctrl+O' },
      { value: 'sep-1', label: '', separator: true },
      {
        value: 'recent',
        label: 'Open Recent',
        submenu: [
          { value: 'r1', label: 'project-alpha.pxl' },
          { value: 'r2', label: 'project-beta.pxl' },
        ],
      },
      { value: 'sep-2', label: '', separator: true },
      { value: 'save', label: 'Save', shortcut: 'Ctrl+S' },
      { value: 'quit', label: 'Quit', shortcut: 'Ctrl+Q' },
    ],
  },
  {
    label: 'Edit',
    items: [
      { value: 'undo', label: 'Undo', shortcut: 'Ctrl+Z' },
      { value: 'redo', label: 'Redo', shortcut: 'Ctrl+Y', disabled: true },
      { value: 'sep-3', label: '', separator: true },
      { value: 'cut', label: 'Cut', shortcut: 'Ctrl+X' },
      { value: 'copy', label: 'Copy', shortcut: 'Ctrl+C' },
      { value: 'paste', label: 'Paste', shortcut: 'Ctrl+V' },
    ],
  },
  {
    label: 'View',
    items: [
      { value: 'zoom-in', label: 'Zoom In', shortcut: 'Ctrl++' },
      { value: 'zoom-out', label: 'Zoom Out', shortcut: 'Ctrl+-' },
    ],
  },
];

export function Default() {
  return <PixelMenubar menus={menus} />;
}

With Submenus

import { PixelMenubar, type PixelMenubarMenu } from '@pxlkit/ui-kit';

export function WithSubmenus() {
  const nested: PixelMenubarMenu[] = [
    {
      label: 'Tools',
      items: [
        {
          value: 'export',
          label: 'Export As',
          submenu: [
            { value: 'png', label: 'PNG' },
            { value: 'svg', label: 'SVG' },
            { value: 'gif', label: 'GIF' },
          ],
        },
        {
          value: 'convert',
          label: 'Convert',
          submenu: [
            { value: 'palette', label: 'Reduce Palette' },
            { value: 'grayscale', label: 'Grayscale' },
          ],
        },
      ],
    },
  ];
  return <PixelMenubar menus={nested} />;
}

Related