Loading Developer Playground

Loading ...

Skip to main content
ARIA ROLEWidget Roles

menuitemradio

A checkable menuitem in a group where only one item can be checked at a time. Perfect for mutually exclusive options like theme selection, sort order, or view modes.

Parent Roles
menu, menubar, group
Required Attribute
aria-checked
Selection Type
Single (exclusive)
Keyboard Support
Enter, Space

Overview

The menuitemradio role defines a menu item that is part of a group of items with the same role, where only one item in the group can be checked at a time—similar to how radio buttons work in forms.

Unlike menuitemcheckbox where multiple items can be selected, menuitemradio enforces mutual exclusivity within its group:

  • aria-checked="true" - This item is selected (only one per group)
  • aria-checked="false" - This item is not selected

When to Use menuitemradio

Use menuitemradio when you have a set of mutually exclusive options in a dropdown menu. Common examples include font size selection, theme switching, sort order options, and view mode selection.

Live Demo: Interactive Radio Menus

Font Size Selection

Theme Selection

Sort Order Selection

Try with keyboard: Open a menu, then use / to navigate, Enter or Space to select, and Escape to close. Notice how only one option can be selected at a time!

Code Examples

Basic Usage

<!-- Basic menuitemradio in a dropdown menu -->
<button 
  aria-haspopup="menu" 
  aria-expanded="false"
  id="font-button"
>
  Font Size
</button>

<div role="menu" aria-labelledby="font-button">
  <div
    role="menuitemradio"
    aria-checked="false"
    tabindex="-1"
  >
    Small
  </div>
  <div
    role="menuitemradio"
    aria-checked="true"
    tabindex="-1"
  >
    Medium
  </div>
  <div
    role="menuitemradio"
    aria-checked="false"
    tabindex="-1"
  >
    Large
  </div>
</div>

<!-- 
  Note: Only ONE menuitemradio in a group should have 
  aria-checked="true" at any time (mutually exclusive)
-->

Multiple Radio Groups

<!-- Multiple radio groups in one menu -->
<div role="menu" aria-label="Document Settings">
  <!-- Group 1: Font Size -->
  <div role="group" aria-label="Font Size">
    <div role="presentation" class="group-label">Font Size</div>
    <div role="menuitemradio" aria-checked="false" tabindex="-1">
      Small
    </div>
    <div role="menuitemradio" aria-checked="true" tabindex="-1">
      Medium
    </div>
    <div role="menuitemradio" aria-checked="false" tabindex="-1">
      Large
    </div>
  </div>
  
  <div role="separator"></div>
  
  <!-- Group 2: Line Spacing -->
  <div role="group" aria-label="Line Spacing">
    <div role="presentation" class="group-label">Line Spacing</div>
    <div role="menuitemradio" aria-checked="false" tabindex="-1">
      Single
    </div>
    <div role="menuitemradio" aria-checked="true" tabindex="-1">
      1.5 Lines
    </div>
    <div role="menuitemradio" aria-checked="false" tabindex="-1">
      Double
    </div>
  </div>
</div>

<!-- 
  Use role="group" with aria-label to create 
  semantic groups within a menu. Each group 
  maintains its own selection independently.
-->

Keyboard Support

<!-- Accessible menuitemradio with keyboard support -->
<script>
  const menu = document.querySelector('[role="menu"]');
  const items = menu.querySelectorAll('[role="menuitemradio"]');
  let currentIndex = 0;
  
  menu.addEventListener('keydown', (e) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        currentIndex = (currentIndex + 1) % items.length;
        items[currentIndex].focus();
        break;
        
      case 'ArrowUp':
        e.preventDefault();
        currentIndex = (currentIndex - 1 + items.length) % items.length;
        items[currentIndex].focus();
        break;
        
      case 'Home':
        e.preventDefault();
        currentIndex = 0;
        items[currentIndex].focus();
        break;
        
      case 'End':
        e.preventDefault();
        currentIndex = items.length - 1;
        items[currentIndex].focus();
        break;
        
      case 'Enter':
      case ' ':
        e.preventDefault();
        selectRadioItem(items[currentIndex]);
        break;
        
      case 'Escape':
        e.preventDefault();
        closeMenu();
        break;
    }
  });
  
  function selectRadioItem(selectedItem) {
    // Get all items in the same group
    const group = selectedItem.closest('[role="group"]') || menu;
    const groupItems = group.querySelectorAll('[role="menuitemradio"]');
    
    // Deselect all items in group
    groupItems.forEach(item => {
      item.setAttribute('aria-checked', 'false');
    });
    
    // Select the clicked item
    selectedItem.setAttribute('aria-checked', 'true');
  }
</script>

React Component

// React menuitemradio Component
import { useState, useRef, useEffect } from 'react';

interface RadioMenuItem {
  id: string;
  label: string;
  value: string;
}

interface MenuRadioGroupProps {
  label: string;
  items: RadioMenuItem[];
  selectedValue: string;
  onChange: (value: string) => void;
}

function MenuRadioGroup({ label, items, selectedValue, onChange }: MenuRadioGroupProps) {
  const [isOpen, setIsOpen] = useState(false);
  const [focusedIndex, setFocusedIndex] = useState(0);
  const menuRef = useRef<HTMLDivElement>(null);
  const buttonRef = useRef<HTMLButtonElement>(null);

  const selectedItem = items.find(i => i.id === selectedValue);

  const handleKeyDown = (e: React.KeyboardEvent) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setFocusedIndex(prev => (prev + 1) % items.length);
        break;
      case 'ArrowUp':
        e.preventDefault();
        setFocusedIndex(prev => 
          (prev - 1 + items.length) % items.length
        );
        break;
      case 'Enter':
      case ' ':
        e.preventDefault();
        onChange(items[focusedIndex].id);
        break;
      case 'Escape':
        setIsOpen(false);
        buttonRef.current?.focus();
        break;
    }
  };

  useEffect(() => {
    if (isOpen) {
      // Focus the selected item when menu opens
      const selectedIndex = items.findIndex(i => i.id === selectedValue);
      setFocusedIndex(selectedIndex >= 0 ? selectedIndex : 0);
      menuRef.current?.focus();
    }
  }, [isOpen, items, selectedValue]);

  return (
    <div className="relative">
      <button
        ref={buttonRef}
        aria-haspopup="menu"
        aria-expanded={isOpen}
        onClick={() => setIsOpen(!isOpen)}
        className="px-4 py-2 bg-gray-800 rounded flex items-center gap-2"
      >
        {label}: {selectedItem?.label}
        <ChevronIcon />
      </button>

      {isOpen && (
        <div
          ref={menuRef}
          role="menu"
          aria-label={label}
          tabIndex={-1}
          onKeyDown={handleKeyDown}
          className="absolute mt-1 bg-gray-900 rounded shadow-lg"
        >
          {items.map((item, index) => (
            <div
              key={item.id}
              role="menuitemradio"
              aria-checked={item.id === selectedValue}
              tabIndex={index === focusedIndex ? 0 : -1}
              onClick={() => {
                onChange(item.id);
                setIsOpen(false);
              }}
              className={`px-4 py-2 flex items-center gap-2 cursor-pointer
                ${index === focusedIndex ? 'bg-purple-600/30' : 'hover:bg-white/10'}
                ${item.id === selectedValue ? 'text-purple-300' : 'text-white'}`}
            >
              <span className="w-4 h-4 rounded-full border-2 flex items-center justify-center
                ${item.id === selectedValue ? 'border-purple-400' : 'border-gray-500'}">
                {item.id === selectedValue && (
                  <span className="w-2 h-2 rounded-full bg-purple-400" />
                )}
              </span>
              {item.label}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// Usage
function App() {
  const [fontSize, setFontSize] = useState('medium');
  
  return (
    <MenuRadioGroup
      label="Font Size"
      items={[
        { id: 'small', label: 'Small', value: '12px' },
        { id: 'medium', label: 'Medium', value: '16px' },
        { id: 'large', label: 'Large', value: '20px' },
      ]}
      selectedValue={fontSize}
      onChange={setFontSize}
    />
  );
}

Mixed with Checkboxes

<!-- Menu with both menuitemcheckbox and menuitemradio -->
<div role="menu" aria-label="View Settings">
  <!-- Checkbox items (multiple can be selected) -->
  <div role="group" aria-label="Panels">
    <div role="presentation">Show Panels</div>
    <div role="menuitemcheckbox" aria-checked="true" tabindex="-1">
      Sidebar
    </div>
    <div role="menuitemcheckbox" aria-checked="true" tabindex="-1">
      Toolbar
    </div>
    <div role="menuitemcheckbox" aria-checked="false" tabindex="-1">
      Status Bar
    </div>
  </div>
  
  <div role="separator"></div>
  
  <!-- Radio items (only one can be selected) -->
  <div role="group" aria-label="View Mode">
    <div role="presentation">View Mode</div>
    <div role="menuitemradio" aria-checked="true" tabindex="-1">
      List View
    </div>
    <div role="menuitemradio" aria-checked="false" tabindex="-1">
      Grid View
    </div>
    <div role="menuitemradio" aria-checked="false" tabindex="-1">
      Compact View
    </div>
  </div>
</div>

<!-- 
  Key difference:
  - menuitemcheckbox: Multiple items can be checked (independent)
  - menuitemradio: Only ONE item per group can be checked (mutually exclusive)
-->

Keyboard Support

EnterSelects the focused item (and deselects others in group)
SpaceSelects the focused item (and deselects others in group)
Moves focus to next menu item
Moves focus to previous menu item
HomeMoves focus to first menu item
EndMoves focus to last menu item
EscapeCloses the menu and returns focus to trigger

Required & Supported Attributes

Required Attributes

aria-checked

Required. Indicates the current selection state. Must be true or false. Unlike menuitemcheckbox, the mixed value is NOT valid for menuitemradio.

Supported Attributes

aria-checkedRequired

Required. Selection state (true/false only)

aria-disabled

Indicates item is disabled and not selectable

aria-label

Accessible name for the menu item

aria-labelledby

References element(s) that label the item

aria-describedby

References element(s) that describe the item

tabindex

Should be -1 (focus managed by menu container)

Best Practices

Always ensure exactly ONE item in a group has aria-checked="true"

Use role="group" with aria-label to create semantic radio groups

Provide visual indication of selected state (filled radio icon)

Update aria-checked immediately when selection changes

Use tabindex="-1" and manage focus via arrow keys

Support both Enter and Space key for selection

When menu opens, focus the currently selected item

×

Don't allow multiple items to have aria-checked="true" simultaneously

×

Don't use aria-checked="mixed" with menuitemradio (not valid)

×

Don't use for multi-select scenarios (use menuitemcheckbox instead)

×

Don't forget to provide visible focus indicators

menuitemradio vs menuitemcheckbox

menuitemradio

  • Only one item can be selected per group
  • No mixed state (true/false only)
  • Example: Font size, Theme, Sort order
  • Uses radio button-style visual (●)
  • Mutually exclusive choices

menuitemcheckbox

  • Multiple items can be selected
  • Supports mixed state (true/false/mixed)
  • Example: Bold, Italic, Show panels
  • Uses checkbox-style visual (✓)
  • Independent toggles

Common Use Cases

Font Settings

Font size, Font family, Line spacing selection

Theme Selection

Light/Dark/System/High contrast modes

Sort Order

Name, Date, Size, Type sorting options

View Mode

List/Grid/Compact/Detail view toggles

Language Selection

Application language preferences

Density Settings

Comfortable/Cozy/Compact display density

Date Format

MM/DD/YYYY, DD/MM/YYYY, etc.

Time Zone

Local/UTC/Specific timezone selection

Accessibility Notes

Screen Reader Announcements

Screen readers announce menuitemradio elements as "radio menu item" followed by the label, position in group (e.g., "1 of 4"), and selection state. For example: "Medium, radio menu item, checked, 2 of 4". This helps users understand both the selection and the available options.

Selection Behavior

When a user selects a menuitemradio, you must update the aria-checked attribute of all items in the group: set the selected item to true and all others to false. This ensures screen readers correctly announce the new selection state.

Visual Distinction

Use a filled circle (●) for selected items and an empty circle (○) for unselected items. This radio-button style visual helps users distinguish menuitemradio from menuitemcheckbox, which should use checkmark (✓) style visuals.

Related Roles & Attributes

Specifications & Resources