Select Page

How to Add ARIA Labels to Custom Dropdown Menus: A Step-by-Step Guide

by | Apr 8, 2026 | Uncategorized

Why Custom Dropdown Menus Need ARIA Labels

Native HTML <select> elements come with built-in accessibility. Screen readers know how to announce them, keyboards can navigate them, and assistive technologies understand their purpose without any extra work from the developer.

But the moment you build a custom dropdown menu using <div>, <ul>, or <span> elements, all of that built-in accessibility disappears. To a screen reader, your beautifully styled dropdown is just a meaningless block of text.

That is where ARIA (Accessible Rich Internet Applications) labels, roles, and properties come in. They bridge the gap between your custom UI and assistive technologies by telling screen readers what each element is, what state it is in, and how it relates to other elements on the page.

In this guide, we will walk through the exact process of making custom dropdown menus accessible. You will see before-and-after code comparisons for every step so you can see precisely how each ARIA attribute changes the experience for users who rely on assistive technology.

What You Need to Know Before You Start

Before diving into implementation, let’s clarify a few foundational concepts.

The First Rule of ARIA

The W3C’s own ARIA documentation states: “If you can use a native HTML element with the semantics and behavior you require already built in, do so.” In other words, if a standard <select> element meets your design needs, use it instead of building a custom dropdown.

However, there are many valid reasons to build custom dropdowns: complex styling requirements, multi-select functionality, search filtering, grouped options, or inserting rich content like icons and descriptions inside each option. When you do need a custom solution, ARIA is essential.

Key ARIA Attributes for Dropdowns

Here is a quick reference of the ARIA attributes we will use throughout this guide:

Attribute Purpose Example
role="listbox" Identifies the dropdown list container Applied to the <ul> that holds options
role="option" Identifies each selectable item Applied to each <li> inside the list
aria-label Provides an accessible name when no visible label exists aria-label="Select a country"
aria-labelledby Points to a visible label element by its ID aria-labelledby="country-label"
aria-expanded Indicates whether the dropdown is open or closed aria-expanded="true" or "false"
aria-haspopup Tells the screen reader that a popup will appear aria-haspopup="listbox"
aria-activedescendant Points to the currently focused option within the list aria-activedescendant="option-2"
aria-selected Indicates which option is currently selected aria-selected="true"

Step 1: Start With the Inaccessible Dropdown (Before)

Let’s start with a common custom dropdown that has zero accessibility support. This is the kind of code you will find in many UI libraries and tutorials that focus only on visual design:

<!-- BEFORE: Inaccessible custom dropdown -->
<div class="dropdown">
  <div class="dropdown-toggle" onclick="toggleDropdown()">
    Select a fruit
  </div>
  <ul class="dropdown-menu">
    <li onclick="selectOption('Apple')">Apple</li>
    <li onclick="selectOption('Banana')">Banana</li>
    <li onclick="selectOption('Cherry')">Cherry</li>
  </ul>
</div>

What a screen reader “sees” here: A group of generic text elements. There is no indication that this is a dropdown, no way to know if it is open or closed, no keyboard support, and no labeling. A screen reader user would have no idea this is an interactive control at all.

Step 2: Add Roles to Define the Structure

The first thing we need to do is tell assistive technologies what these elements are. We do this with ARIA roles.

<!-- AFTER: Roles added -->
<div class="dropdown">
  <button class="dropdown-toggle" onclick="toggleDropdown()">
    Select a fruit
  </button>
  <ul class="dropdown-menu" role="listbox">
    <li role="option" onclick="selectOption('Apple')">Apple</li>
    <li role="option" onclick="selectOption('Banana')">Banana</li>
    <li role="option" onclick="selectOption('Cherry')">Cherry</li>
  </ul>
</div>

What changed:

  • The trigger <div> was replaced with a <button>. This is critical because buttons are natively focusable and activatable with the keyboard. Never use a <div> as a button unless you absolutely must.
  • The <ul> received role="listbox", which tells screen readers this is a list of selectable options.
  • Each <li> received role="option", identifying them as individual choices.

Screen reader experience now: “Select a fruit, button” followed by “listbox” with “Apple, option” etc. This is a huge improvement, but we are not done yet.

Step 3: Add ARIA Labels for Clear Context

Now we need to provide a clear, accessible name for the dropdown so users know what it is for.

You have two choices here:

Option A: Use aria-label (when there is no visible label)

<button 
  class="dropdown-toggle" 
  aria-label="Select a fruit"
  aria-haspopup="listbox"
>
  Select a fruit
</button>

Option B: Use aria-labelledby (when a visible label exists)

<label id="fruit-label">Choose your favorite fruit</label>
<button 
  class="dropdown-toggle" 
  aria-labelledby="fruit-label"
  aria-haspopup="listbox"
>
  Select a fruit
</button>

Which should you use?

  • Use aria-labelledby whenever a visible label exists on the page. This keeps the visual label and the accessible name in sync.
  • Use aria-label when there is no visible label and you need to provide one purely for screen readers.
  • Do not use both on the same element. aria-labelledby takes precedence and aria-label will be ignored.

We also added aria-haspopup="listbox" to the button. This tells the screen reader that activating this button will display a listbox popup.

Step 4: Communicate the Expanded/Collapsed State

Screen reader users need to know whether the dropdown is currently open or closed. The aria-expanded attribute handles this.

<!-- AFTER: State management added -->
<button 
  class="dropdown-toggle" 
  aria-label="Select a fruit"
  aria-haspopup="listbox"
  aria-expanded="false"
>
  Select a fruit
</button>

You must update this attribute dynamically with JavaScript whenever the dropdown opens or closes:

function toggleDropdown() {
  const button = document.querySelector('.dropdown-toggle');
  const menu = document.querySelector('.dropdown-menu');
  const isExpanded = button.getAttribute('aria-expanded') === 'true';
  
  button.setAttribute('aria-expanded', String(!isExpanded));
  menu.style.display = isExpanded ? 'none' : 'block';
}

Screen reader experience now: “Select a fruit, button, collapsed” or “Select a fruit, button, expanded” depending on the state. The user always knows what is happening.

Step 5: Add aria-selected and aria-activedescendant

Next, we need to communicate which option is currently selected and which one is currently focused (these can be different).

<!-- AFTER: Selection tracking added -->
<button 
  class="dropdown-toggle" 
  aria-label="Select a fruit"
  aria-haspopup="listbox"
  aria-expanded="true"
  aria-activedescendant="option-apple"
>
  Apple
</button>
<ul class="dropdown-menu" role="listbox" aria-label="Fruit options">
  <li role="option" id="option-apple" aria-selected="true">Apple</li>
  <li role="option" id="option-banana" aria-selected="false">Banana</li>
  <li role="option" id="option-cherry" aria-selected="false">Cherry</li>
</ul>

What changed:

  • Each option now has a unique id.
  • aria-selected="true" marks the currently chosen option.
  • aria-activedescendant on the button points to the option that currently has visual focus, allowing screen readers to announce the focused option without moving DOM focus away from the button.
  • The listbox itself received aria-label="Fruit options" for additional context.

Step 6: Implement Keyboard Navigation

ARIA attributes alone are not enough. The WAI-ARIA Authoring Practices Guide specifies the keyboard interactions that users expect from a listbox dropdown. Without these, your dropdown is still inaccessible to keyboard-only users.

Here are the expected keyboard interactions:

Key Expected Behavior
Enter / Space Opens the dropdown (if closed) or selects the focused option (if open)
Arrow Down Moves focus to the next option
Arrow Up Moves focus to the previous option
Escape Closes the dropdown and returns focus to the trigger button
Home Moves focus to the first option
End Moves focus to the last option

Here is a JavaScript implementation that handles all of these interactions:

const button = document.querySelector('.dropdown-toggle');
const menu = document.querySelector('.dropdown-menu');
const options = menu.querySelectorAll('[role="option"]');
let currentIndex = -1;

button.addEventListener('keydown', function(e) {
  switch(e.key) {
    case 'ArrowDown':
      e.preventDefault();
      openDropdown();
      moveFocus(0);
      break;
    case 'ArrowUp':
      e.preventDefault();
      openDropdown();
      moveFocus(options.length - 1);
      break;
    case 'Escape':
      closeDropdown();
      break;
  }
});

menu.addEventListener('keydown', function(e) {
  switch(e.key) {
    case 'ArrowDown':
      e.preventDefault();
      moveFocus(Math.min(currentIndex + 1, options.length - 1));
      break;
    case 'ArrowUp':
      e.preventDefault();
      moveFocus(Math.max(currentIndex - 1, 0));
      break;
    case 'Enter':
    case ' ':
      e.preventDefault();
      selectCurrentOption();
      break;
    case 'Escape':
      closeDropdown();
      button.focus();
      break;
    case 'Home':
      e.preventDefault();
      moveFocus(0);
      break;
    case 'End':
      e.preventDefault();
      moveFocus(options.length - 1);
      break;
  }
});

function moveFocus(index) {
  currentIndex = index;
  options.forEach(opt => opt.classList.remove('focused'));
  options[currentIndex].classList.add('focused');
  button.setAttribute('aria-activedescendant', options[currentIndex].id);
}

function selectCurrentOption() {
  options.forEach(opt => opt.setAttribute('aria-selected', 'false'));
  options[currentIndex].setAttribute('aria-selected', 'true');
  button.textContent = options[currentIndex].textContent;
  closeDropdown();
  button.focus();
}

function openDropdown() {
  menu.style.display = 'block';
  button.setAttribute('aria-expanded', 'true');
}

function closeDropdown() {
  menu.style.display = 'none';
  button.setAttribute('aria-expanded', 'false');
}

Step 7: The Complete Accessible Dropdown

Here is the full before-and-after comparison so you can see the total transformation:

Before (Inaccessible)

<div class="dropdown">
  <div class="dropdown-toggle" onclick="toggleDropdown()">
    Select a fruit
  </div>
  <ul class="dropdown-menu">
    <li onclick="selectOption('Apple')">Apple</li>
    <li onclick="selectOption('Banana')">Banana</li>
    <li onclick="selectOption('Cherry')">Cherry</li>
  </ul>
</div>

After (Fully Accessible)

<div class="dropdown">
  <label id="fruit-label">Choose a fruit</label>
  <button 
    class="dropdown-toggle"
    aria-labelledby="fruit-label"
    aria-haspopup="listbox"
    aria-expanded="false"
    aria-activedescendant=""
  >
    Select a fruit
  </button>
  <ul 
    class="dropdown-menu" 
    role="listbox" 
    aria-labelledby="fruit-label"
    tabindex="-1"
  >
    <li role="option" id="option-apple" aria-selected="false">Apple</li>
    <li role="option" id="option-banana" aria-selected="false">Banana</li>
    <li role="option" id="option-cherry" aria-selected="false">Cherry</li>
  </ul>
</div>

Common Mistakes to Avoid

Even experienced developers make these mistakes when adding ARIA to custom dropdowns. Here is what to watch out for:

  1. Using role="menu" instead of role="listbox". The menu role is intended for application-style menus (like right-click context menus or menu bars). For a dropdown that lets users select a value, listbox with option is the correct pattern.
  2. Forgetting to update aria-expanded dynamically. Setting it in the HTML but never changing it with JavaScript means the screen reader will always report the wrong state.
  3. Adding ARIA roles but skipping keyboard navigation. ARIA tells screen readers what something is, but it does not add behavior. You must implement keyboard support separately.
  4. Using aria-label and aria-labelledby on the same element. When both are present, aria-labelledby wins and aria-label is ignored. Pick one.
  5. Not giving each option a unique id. Without unique IDs, aria-activedescendant cannot point to the correct option.
  6. Hiding the dropdown with display: none but forgetting to set aria-expanded="false". Always keep your ARIA state in sync with the visual state.

Testing Your Accessible Dropdown

After implementing all the ARIA attributes and keyboard navigation, you need to test. Here is a practical testing checklist:

  • Screen reader testing: Test with at least one screen reader. NVDA (free, Windows), VoiceOver (built into macOS/iOS), and TalkBack (Android) are the most common. Verify that the dropdown label, state, and options are all announced correctly.
  • Keyboard-only testing: Unplug your mouse and try to use the dropdown using only the keyboard. Can you open it, navigate options, select one, and close it?
  • Automated testing tools: Use browser extensions like axe DevTools, WAVE, or Lighthouse accessibility audits to catch any remaining issues. Keep in mind that automated tools only catch about 30-50% of accessibility problems.
  • Check focus management: When the dropdown closes, does focus return to the trigger button? When it opens, is the focus handled correctly?

Framework-Specific Tips

If you are working with a JavaScript framework, the same ARIA principles apply, but the implementation details vary slightly.

React

In React (and JSX in general), ARIA attributes use the same names as in HTML. No camelCase conversion is needed:

<button
  aria-label="Select a fruit"
  aria-haspopup="listbox"
  aria-expanded={isOpen}
  aria-activedescendant={activeId}
  onClick={toggleDropdown}
  onKeyDown={handleKeyDown}
>
  {selectedFruit || 'Select a fruit'}
</button>

Consider using libraries like React Aria (by Adobe) or Downshift (by Kent C. Dodds) that handle all ARIA patterns for you. These libraries have been extensively tested with real assistive technologies.

Vue

Vue handles ARIA attributes naturally in templates:

<button
  :aria-expanded="String(isOpen)"
  aria-haspopup="listbox"
  :aria-activedescendant="activeOptionId"
  @keydown="handleKeyDown"
>
  {{ selectedOption || 'Select a fruit' }}
</button>

Angular

In Angular, use property binding for dynamic ARIA values:

<button
  [attr.aria-expanded]="isOpen"
  [attr.aria-activedescendant]="activeOptionId"
  aria-haspopup="listbox"
  (keydown)="handleKeyDown($event)"
>
  {{ selectedOption || 'Select a fruit' }}
</button>

Quick Reference: ARIA Dropdown Checklist

Use this checklist every time you build a custom dropdown:

Requirement Attribute/Approach Done?
Trigger is a <button> (or has role="button" + tabindex="0") Native HTML
Dropdown trigger has an accessible name aria-label or aria-labelledby
Trigger announces popup type aria-haspopup="listbox"
Open/closed state is communicated aria-expanded
List container has correct role role="listbox"
Each item has correct role role="option"
Each item has a unique ID id="option-xxx"
Selected item is marked aria-selected="true"
Focused item is tracked aria-activedescendant
Full keyboard navigation works JavaScript event handlers
Focus returns to button on close JavaScript focus management

Frequently Asked Questions

What is the difference between aria-label and aria-labelledby?

aria-label provides an accessible name as a string directly on the element. aria-labelledby references the id of another element on the page that serves as the label. Use aria-labelledby when you have a visible label and aria-label when you do not.

Should I use role=”menu” or role=”listbox” for a custom dropdown?

For a dropdown where users select a value (like a form select), use role="listbox" with role="option" for each item. The role="menu" pattern is intended for application-style action menus (like navigation menus or context menus) where each item triggers an action rather than setting a value.

Can I add ARIA labels using CSS?

No. CSS cannot add or modify ARIA attributes. ARIA attributes must be set in the HTML or manipulated through JavaScript. CSS can style elements based on ARIA attributes (e.g., [aria-expanded="true"] { display: block; }), but it cannot create them.

Do I still need ARIA if I use a native select element?

No. Native <select> elements have full accessibility support built in. You only need ARIA when building custom dropdown components from non-semantic HTML elements like <div> or <ul>.

How do I test if my ARIA labels are working correctly?

The most reliable way is to test with an actual screen reader. On Windows, use NVDA (free) or JAWS. On macOS, use VoiceOver (built in, activated with Cmd+F5). Additionally, use browser dev tools: in Chrome, the Accessibility tab in the Elements panel shows you the computed accessible name and role for any element.

Does adding ARIA slow down page performance?

No. ARIA attributes have zero impact on rendering performance. They are metadata that the browser’s accessibility API reads and passes to assistive technologies. There is no performance reason to skip ARIA.

What WCAG success criteria do accessible dropdowns satisfy?

Properly implemented custom dropdowns with ARIA help you meet several WCAG 2.2 success criteria, including 1.3.1 (Info and Relationships), 2.1.1 (Keyboard), 4.1.2 (Name, Role, Value), and 2.4.7 (Focus Visible).

Wrapping Up

Adding ARIA labels to custom dropdown menus is not just about compliance. It is about making your application usable for everyone. The process follows a clear pattern: define what each element is with roles, name it with labels, communicate its state with properties, and add keyboard behavior with JavaScript.

The key takeaway is that ARIA attributes and keyboard navigation must work together. ARIA tells assistive technologies what your dropdown is and what is happening. Keyboard navigation makes it actually usable. One without the other still leaves your dropdown inaccessible.

Start with the checklist above, test with a real screen reader, and you will have a custom dropdown that works for all of your users.

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *