References

Beginner-friendly references for web development, with live, editable examples.

The HTML aria-haspopup attribute

ARIA Accessibility
In short

The aria-haspopup attribute says a control can open an interactive popup, and its value names the type. Valid types are menu, listbox, tree, grid and dialog. The value true is an older spelling of menu, and false, the default, means no popup.

Overview

The aria-haspopup property marks an element as able to open an interactive popup, and names the popup's type. A popup here is a menu, listbox, tree, grid or dialog, and a tooltip doesn't count as one in this sense.

The popup has to match the value. The popup's container must have one of those five roles, and aria-haspopup must match that role. The attribute itself opens nothing and moves no focus.

Global use is deprecated in WAI-ARIA 1.2. The application, button, combobox, gridcell, link, menuitem, slider, tab, textbox and treeitem roles still list it. Inheritance adds columnheader, menuitemcheckbox, menuitemradio, rowheader and searchbox.

Other roles aren't cut off yet, because the spec leaves that limit to future versions. For now, browsers still expose it on a heading, a region and a plain <div>, so a value in the accessibility tree isn't proof that the role specifically supports it.

Any value outside the list, including an empty string, must be treated as false under the spec. Chrome's tree follows that, while Firefox's does so for an empty string but still reports a popup for a word such as tooltip.

Syntax

<button aria-haspopup="menu" aria-expanded="false">Export</button>
<ul role="menu" aria-label="Export formats" hidden>
  <li role="menuitem" tabindex="-1">CSV</li>
</ul>

Values

Value
false | true | menu | listbox | tree | grid | dialog

Example

Live example
<p style="margin:0 0 8px">Open the menu, then use the Up and Down arrow keys, Enter or Escape.</p>
<button type="button" id="export" aria-haspopup="menu" aria-expanded="false" aria-controls="formats">Export <span aria-hidden="true">&#9662;</span></button>
<ul id="formats" role="menu" aria-labelledby="export" style="list-style:none;margin:4px 0 0;padding:4px;width:120px;border:1px solid #767676;border-radius:6px" hidden>
  <li role="menuitem" tabindex="-1" style="padding:2px 6px">CSV</li>
  <li role="menuitem" tabindex="-1" style="padding:2px 6px">JSON</li>
  <li role="menuitem" tabindex="-1" style="padding:2px 6px">PDF</li>
</ul>
<p role="status" id="state" style="margin:10px 0 0">Menu closed. aria-haspopup is menu and aria-expanded is false.</p>
<script>
const button = document.getElementById('export');
const menu = document.getElementById('formats');
const items = [...menu.querySelectorAll('[role="menuitem"]')];
const state = document.getElementById('state');
function openMenu() {
  menu.hidden = false;
  button.setAttribute('aria-expanded', 'true');
  items[0].focus();
  state.textContent = 'Menu open. aria-haspopup is still menu, and aria-expanded is now true.';
}
function closeMenu(message) {
  menu.hidden = true;
  button.setAttribute('aria-expanded', 'false');
  button.focus();
  state.textContent = message;
}
button.addEventListener('click', function () {
  if (menu.hidden) openMenu();
  else closeMenu('Menu closed. aria-expanded is false again.');
});
button.addEventListener('keydown', function (event) {
  if (event.key === 'ArrowDown' && menu.hidden) {
    event.preventDefault();
    openMenu();
  }
});
items.forEach(function (item, index) {
  item.addEventListener('click', function () { closeMenu('Export format set to ' + item.textContent + '.'); });
  item.addEventListener('keydown', function (event) {
    const step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;
    if (step) {
      event.preventDefault();
      items[(index + step + items.length) % items.length].focus();
    } else if (event.key === 'Enter' || event.key === ' ') {
      event.preventDefault();
      item.click();
    } else if (event.key === 'Escape' || event.key === 'Tab') {
      closeMenu('Menu closed. aria-expanded is false again.');
    }
  });
});
document.addEventListener('click', function (event) {
  if (!menu.hidden && !menu.contains(event.target) && !button.contains(event.target)) closeMenu('Menu closed. aria-expanded is false again.');
});
</script>

Best practices

  • Add aria-haspopup where the control shows a visual cue, such as a chevron or ellipsis. When nothing visible hints at a popup, weigh whether the attribute is needed at all.
  • Make the trigger reachable and operable by keyboard. Keep it focusable, provide a key that opens the popup, and have the popup manage focus of its descendants.
  • Use a real <button> to open a menu. A custom control that acts as a menu button should at least have the button role.
  • Set aria-haspopup="menu" on a parent menuitem in a menubar too, and on a tab that opens a popup menu.
  • Reach for <select> when a plain drop-down list is all you need. Its popup is exposed without any attribute.

Frequently asked questions

What is aria-haspopup for?
The aria-haspopup attribute tells assistive technology which kind of popup an element can trigger, such as a menu. Your script still has to open it.
Is aria-haspopup=true the same as aria-haspopup=menu?
Yes, aria-haspopup with the value true is equivalent to menu. The value true exists for backward compatibility with ARIA 1.0 content, so menu is the clearer choice in new code.
Which controls need aria-haspopup=listbox?
Set aria-haspopup to listbox on a control that opens a listbox, unless it's a combobox. The combobox role already has listbox as its implicit value, and needs an explicit value only for another popup role.
Should a button that opens a dialog use aria-haspopup=dialog?
Yes, aria-haspopup set to dialog fits a button that opens an interactive dialog, provided the dialog has that role. A native dialog element has it by default.
Is aria-haspopup deprecated?
Only the global use of aria-haspopup is deprecated, in WAI-ARIA 1.2. Deprecated here means still allowed and expected to work, but recommended against in new content.
How does aria-haspopup relate to aria-expanded?
The aria-haspopup property stays the same, while aria-expanded changes as the popup opens and closes. One names the kind of popup, and the other says whether it's showing.
Does a popovertarget button report aria-haspopup?
No, a popovertarget button gets no aria-haspopup value from the browser. That button is exposed as expanded or collapsed, but not as having a popup.