Loading Developer Playground

Loading ...

Skip to main content
ARIA ROLEβ€’Widget Roles

menuitem

An option in a set of choices contained by a menu or menubar. Menuitems are actionable elements within menus that trigger commands or navigate to destinations.

Parent Container
menu or menubar
Keyboard Support
↑ ↓ Enter Space
Related Roles
menuitemcheckbox, menuitemradio

Overview

The menuitem role represents a single action or command within a menu. When activated, a menuitem performs an action, such as executing a command, opening a dialog, or navigating to another resource.

Important: Menuitems must be contained within an element with role="menu" or role="menubar". Unlike buttons, menuitems use arrow keys for navigation between items, not Tab.

Menu vs. Navigation Links

ARIA menus are for application-style menus (like desktop apps), NOT for site navigation. For navigation menus, use <nav> with links. Use role="menu" only for action menus like File, Edit, or context menus.

Live Demo: Menu with Menuitems

Try it: Click the button or press Enter / ↓ to open. Use ↑ ↓ to navigate, Enter to select, Escape to close.

Code Examples

Basic Menu Structure

<!-- Basic menu with menuitems -->
<div role="menu" aria-label="File menu">
  <div role="menuitem" tabindex="-1">New</div>
  <div role="menuitem" tabindex="-1">Open</div>
  <div role="menuitem" tabindex="-1">Save</div>
  <div role="separator"></div>
  <div role="menuitem" tabindex="-1">Exit</div>
</div>

<!-- Important: Only one menuitem should be in the tab order -->
<!-- Use tabindex="-1" for all items, manage focus with JavaScript -->

Dropdown Menu Pattern

<!-- Dropdown menu pattern -->
<div class="menu-container">
  <button 
    id="menu-button"
    aria-haspopup="menu"
    aria-expanded="false"
    aria-controls="file-menu"
  >
    File
  </button>
  
  <div 
    id="file-menu"
    role="menu"
    aria-labelledby="menu-button"
    hidden
  >
    <div role="menuitem" tabindex="-1">New File</div>
    <div role="menuitem" tabindex="-1">Open...</div>
    <div role="menuitem" tabindex="-1">Save</div>
    <div role="separator"></div>
    <div role="menuitem" tabindex="-1">Print</div>
  </div>
</div>

<script>
  const button = document.getElementById('menu-button');
  const menu = document.getElementById('file-menu');
  
  button.addEventListener('click', () => {
    const isOpen = button.getAttribute('aria-expanded') === 'true';
    button.setAttribute('aria-expanded', !isOpen);
    menu.hidden = isOpen;
    
    if (!isOpen) {
      // Focus first menuitem when opening
      menu.querySelector('[role="menuitem"]').focus();
    }
  });
</script>

Keyboard Navigation

<!-- Menuitem with full keyboard navigation -->
<div 
  role="menu"
  aria-label="Actions menu"
  onkeydown="handleMenuKeydown(event)"
>
  <div 
    role="menuitem" 
    tabindex="-1"
    id="menu-item-0"
    aria-describedby="shortcut-0"
  >
    <span>Cut</span>
    <span id="shortcut-0" class="shortcut">Ctrl+X</span>
  </div>
  <div 
    role="menuitem" 
    tabindex="-1"
    id="menu-item-1"
    aria-describedby="shortcut-1"
  >
    <span>Copy</span>
    <span id="shortcut-1" class="shortcut">Ctrl+C</span>
  </div>
  <div 
    role="menuitem" 
    tabindex="-1"
    id="menu-item-2"
    aria-describedby="shortcut-2"
  >
    <span>Paste</span>
    <span id="shortcut-2" class="shortcut">Ctrl+V</span>
  </div>
</div>

<script>
function handleMenuKeydown(event) {
  const menu = event.currentTarget;
  const items = menu.querySelectorAll('[role="menuitem"]');
  const currentIndex = Array.from(items).indexOf(document.activeElement);
  
  switch (event.key) {
    case 'ArrowDown':
      event.preventDefault();
      const nextIndex = (currentIndex + 1) % items.length;
      items[nextIndex].focus();
      break;
      
    case 'ArrowUp':
      event.preventDefault();
      const prevIndex = currentIndex - 1 < 0 ? items.length - 1 : currentIndex - 1;
      items[prevIndex].focus();
      break;
      
    case 'Home':
      event.preventDefault();
      items[0].focus();
      break;
      
    case 'End':
      event.preventDefault();
      items[items.length - 1].focus();
      break;
      
    case 'Enter':
    case ' ':
      event.preventDefault();
      document.activeElement.click();
      break;
      
    case 'Escape':
      // Close menu and return focus to trigger
      closeMenu();
      break;
  }
}
</script>

Disabled Menuitems

<!-- Menuitem with disabled state -->
<div role="menu" aria-label="Edit menu">
  <div role="menuitem" tabindex="-1">
    Undo
  </div>
  <div role="menuitem" tabindex="-1">
    Redo
  </div>
  <div role="separator"></div>
  <div 
    role="menuitem" 
    tabindex="-1"
    aria-disabled="true"
    class="menu-item-disabled"
  >
    Cut (no selection)
  </div>
  <div 
    role="menuitem" 
    tabindex="-1"
    aria-disabled="true"
    class="menu-item-disabled"
  >
    Copy (no selection)
  </div>
  <div role="menuitem" tabindex="-1">
    Paste
  </div>
</div>

<style>
  .menu-item-disabled {
    color: #999;
    cursor: not-allowed;
  }
</style>

<!-- Note: Disabled items should still be focusable but not activatable -->

Submenu Pattern

<!-- Menuitem that opens a submenu -->
<div role="menu" aria-label="View menu">
  <div role="menuitem" tabindex="-1">Zoom In</div>
  <div role="menuitem" tabindex="-1">Zoom Out</div>
  <div role="separator"></div>
  
  <!-- Submenu trigger -->
  <div 
    role="menuitem" 
    tabindex="-1"
    aria-haspopup="menu"
    aria-expanded="false"
    aria-controls="appearance-submenu"
  >
    Appearance
    <span aria-hidden="true">β–Ά</span>
  </div>
  
  <!-- Submenu -->
  <div 
    id="appearance-submenu"
    role="menu"
    aria-label="Appearance options"
    hidden
  >
    <div role="menuitem" tabindex="-1">Light Theme</div>
    <div role="menuitem" tabindex="-1">Dark Theme</div>
    <div role="menuitem" tabindex="-1">System Default</div>
  </div>
</div>

<!-- Keyboard navigation for submenus:
     - ArrowRight: Open submenu
     - ArrowLeft: Close submenu and return to parent
     - Escape: Close current menu level -->

React Component

// React Menu Component with Menuitem
import { useState, useRef, useEffect } from 'react';

function DropdownMenu({ label, items }) {
  const [isOpen, setIsOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);
  const menuRef = useRef(null);
  const buttonRef = useRef(null);

  // Close menu when clicking outside
  useEffect(() => {
    const handleClickOutside = (e) => {
      if (menuRef.current && !menuRef.current.contains(e.target)) {
        setIsOpen(false);
        setActiveIndex(-1);
      }
    };
    document.addEventListener('mousedown', handleClickOutside);
    return () => document.removeEventListener('mousedown', handleClickOutside);
  }, []);

  const handleButtonKeyDown = (e) => {
    if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      setIsOpen(true);
      setActiveIndex(0);
    }
  };

  const handleMenuKeyDown = (e) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setActiveIndex((prev) => (prev + 1) % items.length);
        break;
      case 'ArrowUp':
        e.preventDefault();
        setActiveIndex((prev) => (prev - 1 + items.length) % items.length);
        break;
      case 'Home':
        e.preventDefault();
        setActiveIndex(0);
        break;
      case 'End':
        e.preventDefault();
        setActiveIndex(items.length - 1);
        break;
      case 'Enter':
      case ' ':
        e.preventDefault();
        if (activeIndex >= 0) {
          items[activeIndex].onClick?.();
          setIsOpen(false);
          buttonRef.current?.focus();
        }
        break;
      case 'Escape':
        setIsOpen(false);
        setActiveIndex(-1);
        buttonRef.current?.focus();
        break;
    }
  };

  return (
    <div ref={menuRef} className="relative">
      <button
        ref={buttonRef}
        aria-haspopup="menu"
        aria-expanded={isOpen}
        onClick={() => setIsOpen(!isOpen)}
        onKeyDown={handleButtonKeyDown}
        className="px-4 py-2 bg-gray-800 text-white rounded"
      >
        {label}
      </button>
      
      {isOpen && (
        <div
          role="menu"
          aria-label={`${label} menu`}
          onKeyDown={handleMenuKeyDown}
          className="absolute mt-1 bg-gray-800 rounded shadow-lg"
        >
          {items.map((item, index) => (
            item.type === 'separator' ? (
              <div key={index} role="separator" className="border-t border-gray-700 my-1" />
            ) : (
              <div
                key={index}
                role="menuitem"
                tabIndex={-1}
                ref={(el) => activeIndex === index && el?.focus()}
                onClick={() => {
                  item.onClick?.();
                  setIsOpen(false);
                  buttonRef.current?.focus();
                }}
                aria-disabled={item.disabled}
                className={`px-4 py-2 cursor-pointer ${
                  activeIndex === index ? 'bg-blue-600' : 'hover:bg-gray-700'
                } ${item.disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
              >
                {item.label}
                {item.shortcut && (
                  <span className="ml-auto text-gray-400 text-sm">{item.shortcut}</span>
                )}
              </div>
            )
          ))}
        </div>
      )}
    </div>
  );
}

// Usage
<DropdownMenu
  label="File"
  items={[
    { label: 'New', shortcut: 'Ctrl+N', onClick: () => console.log('New') },
    { label: 'Open', shortcut: 'Ctrl+O', onClick: () => console.log('Open') },
    { label: 'Save', shortcut: 'Ctrl+S', onClick: () => console.log('Save') },
    { type: 'separator' },
    { label: 'Export', disabled: true },
    { label: 'Exit', onClick: () => console.log('Exit') },
  ]}
/>

Keyboard Support

↓ / ↑→Navigate to next/previous menuitem

Arrow keys move focus between menuitems. Focus should wrap from last to first (and vice versa).

Enter / Space→Activate the focused menuitem

Both keys should activate menuitems. The menu typically closes after activation.

Home / End→Jump to first/last menuitem

Quick navigation to the beginning or end of the menu.

Escape→Close the menu

Closes the menu and returns focus to the trigger element (button/menuitem).

β†’ / ←→Open/close submenus (if applicable)

Right arrow opens a submenu, left arrow closes it and returns to the parent menu.

A-Z→Type-ahead search (optional)

Typing characters can focus the first menuitem starting with that letter.

Best Practices

βœ“

Always nest menuitems inside a menu or menubar element

βœ“

Use arrow keys for navigation between menuitems, not Tab

βœ“

Set tabindex="-1" on all menuitems and manage focus programmatically

βœ“

Return focus to the menu trigger when the menu closes

βœ“

Display keyboard shortcuts visually and describe with aria-describedby

βœ“

Use role="separator" for visual dividers between menu sections

Γ—

Don't use menu/menuitem for site navigation - use nav with links instead

Γ—

Don't allow Tab to navigate between menuitems

Γ—

Don't use menuitem outside of a menu or menubar container

Γ—

Don't forget to close the menu and restore focus on Escape

Supported ARIA Attributes

Common Use Cases

Application menubar (File, Edit, View)
Context menus (right-click menus)
Action dropdown menus
Text editor toolbars
Rich text formatting menus
User account menus
Settings/preferences menus
Export/download option menus

Accessibility Notes

Focus Management

When the menu opens, focus should move to the first menuitem. When closed, focus must return to the element that triggered the menu. All menuitems should have tabindex="-1" so they're not in the natural tab orderβ€”navigation is done with arrow keys.

Screen Reader Announcements

Screen readers announce "menu" when entering and the menuitem name plus its position (e.g., "Save, 3 of 5"). If there's a keyboard shortcut, use aria-describedby to associate it with the menuitem so it's announced.

Visual Focus Indicator

The currently focused menuitem should have a clear visual indicator (highlighted background, outline, etc.). This is especially important for keyboard users who navigate with arrow keys. The indicator should have at least 3:1 contrast ratio.

Disabled Items

Disabled menuitems should use aria-disabled="true" and remain focusable (so users know they exist), but should not be activatable. They should have a visual style that indicates the disabled state (often grayed out).

Related Roles & Attributes

Specifications & Resources