Loading Developer Playground

Loading ...

Skip to main content
ARIA ROLEβ€’Widget Roles

option

A selectable item in a listbox. Options represent individual choices that users can select within dropdown menus, comboboxes, and custom select components.

Parent Container
listbox, group
Required Attributes
aria-selected
Native HTML Equivalent
<option>

Overview

The option role defines a selectable item within a listbox. It represents one choice among multiple possibilities that a user can select. Options are fundamental building blocks for custom dropdown menus, comboboxes, and multi-select components.

Important: Elements with the option role must be contained within an element with role="listbox" or be an owned element of a listbox via aria-owns.

Native <option> vs role="option"

When possible, use native HTML <select> with <option> elements. They provide built-in keyboard navigation, screen reader support, and mobile accessibility. Use role="option" only when you need custom styling or functionality that native elements cannot provide.

Live Demo: Option Interactions

Single Select Listbox

🍎Apple
🍌Banana
πŸ’Cherry
πŸ‡Grape

Selected: apple

Multi-Select Listbox

React
Vue
Angular
Svelte

Selected: react

Screen reader behavior: When navigating options, screen readers announce "[option name], selected" or "[option name], not selected" based on the aria-selected state.

Code Examples

Basic Listbox with Options

<!-- Basic listbox with option roles -->
<div role="listbox" aria-label="Choose a fruit">
  <div role="option" id="opt1" aria-selected="true">
    Apple
  </div>
  <div role="option" id="opt2" aria-selected="false">
    Banana
  </div>
  <div role="option" id="opt3" aria-selected="false">
    Cherry
  </div>
</div>

<!-- Note: Always use aria-selected to indicate selection state -->

Native HTML (Preferred)

<!-- Prefer native HTML when possible -->
<select aria-label="Choose a fruit">
  <option value="apple" selected>Apple</option>
  <option value="banana">Banana</option>
  <option value="cherry">Cherry</option>
</select>

<!-- Native <option> elements automatically have 
     the correct semantics for screen readers -->

Multi-Select Listbox

<!-- Multi-select listbox with multiple options -->
<div 
  role="listbox" 
  aria-label="Select frameworks"
  aria-multiselectable="true"
>
  <div role="option" aria-selected="true" id="react">
    React
  </div>
  <div role="option" aria-selected="false" id="vue">
    Vue
  </div>
  <div role="option" aria-selected="true" id="angular">
    Angular
  </div>
  <div role="option" aria-selected="false" id="svelte">
    Svelte
  </div>
</div>

Keyboard Navigation

<!-- Accessible listbox with keyboard navigation -->
<div 
  role="listbox" 
  aria-label="Select color"
  aria-activedescendant="color-blue"
  tabindex="0"
>
  <div role="option" id="color-red" aria-selected="false">
    Red
  </div>
  <div role="option" id="color-blue" aria-selected="true">
    Blue
  </div>
  <div role="option" id="color-green" aria-selected="false">
    Green
  </div>
</div>

<script>
  const listbox = document.querySelector('[role="listbox"]');
  const options = listbox.querySelectorAll('[role="option"]');
  let currentIndex = 1; // Blue is initially focused

  listbox.addEventListener('keydown', (e) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        currentIndex = Math.min(currentIndex + 1, options.length - 1);
        updateFocus();
        break;
      case 'ArrowUp':
        e.preventDefault();
        currentIndex = Math.max(currentIndex - 1, 0);
        updateFocus();
        break;
      case 'Enter':
      case ' ':
        e.preventDefault();
        selectOption(options[currentIndex]);
        break;
      case 'Home':
        e.preventDefault();
        currentIndex = 0;
        updateFocus();
        break;
      case 'End':
        e.preventDefault();
        currentIndex = options.length - 1;
        updateFocus();
        break;
    }
  });

  function updateFocus() {
    listbox.setAttribute('aria-activedescendant', options[currentIndex].id);
  }

  function selectOption(option) {
    options.forEach(opt => opt.setAttribute('aria-selected', 'false'));
    option.setAttribute('aria-selected', 'true');
  }
</script>

Disabled Options

<!-- Option with disabled state -->
<div role="listbox" aria-label="Choose plan">
  <div role="option" aria-selected="false">
    Free Plan
  </div>
  <div role="option" aria-selected="true">
    Pro Plan
  </div>
  <div role="option" aria-selected="false" aria-disabled="true">
    Enterprise Plan (Coming Soon)
  </div>
</div>

<style>
  [role="option"][aria-disabled="true"] {
    opacity: 0.5;
    cursor: not-allowed;
  }
</style>

React Component

// React Listbox Component with Options
import { useState, useRef, useEffect } from 'react';

function Listbox({ options, label, multiselect = false }) {
  const [selected, setSelected] = useState(
    multiselect ? [] : options[0]?.value
  );
  const [activeIndex, setActiveIndex] = useState(0);
  const listboxRef = useRef(null);

  const handleSelect = (value) => {
    if (multiselect) {
      setSelected(prev =>
        prev.includes(value)
          ? prev.filter(v => v !== value)
          : [...prev, value]
      );
    } else {
      setSelected(value);
    }
  };

  const handleKeyDown = (e) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setActiveIndex(i => Math.min(i + 1, options.length - 1));
        break;
      case 'ArrowUp':
        e.preventDefault();
        setActiveIndex(i => Math.max(i - 1, 0));
        break;
      case 'Enter':
      case ' ':
        e.preventDefault();
        if (!options[activeIndex].disabled) {
          handleSelect(options[activeIndex].value);
        }
        break;
      case 'Home':
        e.preventDefault();
        setActiveIndex(0);
        break;
      case 'End':
        e.preventDefault();
        setActiveIndex(options.length - 1);
        break;
    }
  };

  const isSelected = (value) =>
    multiselect ? selected.includes(value) : selected === value;

  return (
    <div
      ref={listboxRef}
      role="listbox"
      aria-label={label}
      aria-multiselectable={multiselect}
      aria-activedescendant={`option-${activeIndex}`}
      tabIndex={0}
      onKeyDown={handleKeyDown}
      className="listbox"
    >
      {options.map((option, index) => (
        <div
          key={option.value}
          id={`option-${index}`}
          role="option"
          aria-selected={isSelected(option.value)}
          aria-disabled={option.disabled}
          onClick={() => !option.disabled && handleSelect(option.value)}
          className={`option ${
            isSelected(option.value) ? 'selected' : ''
          } ${activeIndex === index ? 'focused' : ''}`}
        >
          {option.label}
        </div>
      ))}
    </div>
  );
}

// Usage
<Listbox
  label="Select your favorite framework"
  multiselect={true}
  options={[
    { value: 'react', label: 'React' },
    { value: 'vue', label: 'Vue' },
    { value: 'angular', label: 'Angular' },
    { value: 'svelte', label: 'Svelte' },
  ]}
/>

Combobox Pattern

<!-- Option within a combobox pattern -->
<label id="combo-label">Search country</label>
<div class="combobox-wrapper">
  <input
    type="text"
    role="combobox"
    aria-labelledby="combo-label"
    aria-expanded="true"
    aria-controls="country-listbox"
    aria-activedescendant="country-usa"
  />
  <ul 
    role="listbox" 
    id="country-listbox"
    aria-labelledby="combo-label"
  >
    <li role="option" id="country-uk" aria-selected="false">
      United Kingdom
    </li>
    <li role="option" id="country-usa" aria-selected="true">
      United States
    </li>
    <li role="option" id="country-canada" aria-selected="false">
      Canada
    </li>
  </ul>
</div>

Keyboard Support

Keyboard interaction is managed by the parent listbox, not individual options. Here are the expected keyboard behaviors:

↓ Down Arrowβ†’Moves focus to the next option

If focus is on the last option, wrapping behavior is optional.

↑ Up Arrowβ†’Moves focus to the previous option

If focus is on the first option, wrapping behavior is optional.

Home→Moves focus to the first option
End→Moves focus to the last option
Enter/Space
β†’Selects the focused option

For multi-select, toggles the selection state without affecting other selected options.

Type a character→Type-ahead search

Moves focus to the next option starting with the typed character(s).

Best Practices

βœ“

Always set aria-selected to explicitly indicate selection state (true or false)

βœ“

Ensure options are contained within a listbox or owned via aria-owns

βœ“

Use native <select> and <option> elements when possible

βœ“

Provide visual indication for focused and selected states

βœ“

Use aria-multiselectable on listbox for multi-select scenarios

βœ“

Implement aria-activedescendant on the listbox for keyboard navigation

Γ—

Don't use option role outside of a listbox context

Γ—

Don't forget to update aria-selected when selection changes

Γ—

Don't make options focusable with tabindex - focus should be on the listbox

Γ—

Don't use aria-checked instead of aria-selected (they have different semantics)

Supported ARIA Attributes

aria-selectedRequired

Required. Indicates selection state (true/false)

aria-disabled

Indicates the option is not selectable

aria-label

Provides accessible name if text content is insufficient

aria-labelledby

References element(s) that label the option

aria-describedby

References element(s) providing additional description

aria-setsize

Total number of options in the list (for virtualized lists)

aria-posinset

Position of the option in the set (for virtualized lists)

aria-checked

Used in certain contexts like tree items (not typical for option)

Common Use Cases

Custom dropdown/select menus
Multi-select tag pickers
Autocomplete/combobox suggestions
Settings and preference selectors
Color/theme pickers
Language selectors
Filter menus in data tables
Country/region selectors

Accessibility Notes

Focus Management

Individual options should not receive direct focus via tabindex. Instead, the parent listbox receives focus, and aria-activedescendant is used to indicate which option is currently highlighted. This allows screen readers to announce the focused option while maintaining a single tab stop.

Visual States

Provide clear visual distinction between: (1) focused option (the option keyboard users are currently on), (2) selected option(s), and (3) default/unselected state. Don't rely solely on color to indicate these statesβ€”use additional visual cues like checkmarks, borders, or background patterns.

Screen Reader Announcements

Screen readers announce options with their text content followed by selection state: "Apple, selected" or "Banana, not selected". For multi-select listboxes, they also announce "multi-selectable" when the listbox receives focus. Ensure option text is meaningful and distinguishable.

Mobile Accessibility

On touch devices, ensure options have adequate touch targets (at least 44Γ—44 CSS pixels). Consider using native <select> elements for mobile as they provide native picker interfaces that are highly optimized for touch interaction.

Related Roles & Attributes

Specifications & Resources