The HTML aria-haspopup attribute
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
<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">▾</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-haspopupwhere 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 parentmenuitemin a menubar too, and on atabthat opens a popup menu. - Reach for <select> when a plain drop-down list is all you need. Its popup is exposed without any attribute.























