aria-haspopup
What is aria-haspopup?
The aria-haspopup attribute tells assistive technologies that activating an element opens an interactive popup and identifies which type: a menu, listbox, tree, grid, or dialog. Screen reader users hear this in advance, so they know what kind of popup to expect before they open it. It’s part of WAI-ARIA, the Web Accessibility Initiative's Accessible Rich Internet Applications spec.
At a glance
Why is aria-haspopup important when it comes to accessibility?
Sighted users get a lot of visual cues for interactive elements on a web page. A small chevron next to a button, an underline, a button styled to look like it opens something – all of it signals that an element triggers more content before a single click happens.
Screen reader users don't get those visual cues from styling alone. Without aria-haspopup, a button element that opens a dropdown menu, a combobox listbox, or a confirmation dialog is announced as nothing more than a plain button, with no warning that pressing it will reveal popup content.
The aria-haspopup attribute is the solution. It tells assistive-technology users two things at once: that the element has a popup, and what kind of interactive popup it opens.
Both pieces of information are equally important, as a menu behaves differently from a dialog once it's open, and screen readers can adjust their navigation commands once they know which one to expect.
It’s most useful on custom dropdown menus, select-style listboxes, and buttons that open dialogs. Site navigation is the exception: navigation dropdowns are usually better built as simple show/hide toggles using aria-expanded alone, since aria-haspopup="menu" promises app-style keyboard behavior most site navs don’t have.
Missing or mismatched popup cues are worth checking for during an accessibility audit of custom dropdown components.
The attribute mainly helps screen reader users. Sighted keyboard and voice control users don’t hear ARIA announcements, so they still rely on visible cues – which is why a dropdown trigger needs both the attribute and a visual indicator, like a chevron icon.
There is support for aria-haspopup across modern browsers and screen readers, though exactly how a popup type is announced can vary depending on the specific browser and assistive technology pairing in use, which is worth confirming with manual testing rather than assuming.
How does aria-haspopup work?
Knowing when to use WAI-ARIA aria-haspopup comes down to one question: does activating this element open one of the five interactive popup types the attribute covers? If the answer is yes, aria-haspopup belongs on the triggering element, typically a button element or a link, so assistive technology users get the same advance notice as sighted users.
A "popup" element refers to one of five interactive components that open over the page: a menu, listbox, tree, grid, or dialog. Each one needs its own role and an accessible name.
The aria-haspopup property accepts false, plus six enumerated values that each indicate a popup: true, menu, listbox, tree, grid, and dialog. false is the default state – if the attribute is left off an element entirely, that's the default, and assistive technologies won't announce a popup at all.
The value true is treated the same as menu, so aria-haspopup="true" and aria-haspopup="menu" tell assistive technologies the exact same thing. Any other value, including an empty string or a role outside this list, is treated as if false were set.
Whichever value is set, the element that pops up needs a matching role. Set aria-haspopup="menu" on the trigger, and the popup container needs role="menu". Set aria-haspopup="dialog" instead, and the popup needs role="dialog" – or a native <dialog> element, which carries that role automatically.
Screen readers rely on the aria-haspopup value matching the popup's actual role; mismatch the two and assistive technology users get told to expect one type of interactive popup when a different one actually opens.
A handful of roles carry an implicit aria-haspopup value without it being added at all. An element with role="combobox", for example, defaults to an implicit value of listbox, since a combobox's popup is a listbox of options by definition. Besides button and link, the attribute is also used in, or inherited into, several other roles, including menuitem, tab, gridcell, treeitem, textbox, slider, and searchbox.
A tooltip is not considered a popup in this context, since it isn't interactive, so aria-haspopup doesn't apply to tooltip triggers.
Setting aria-haspopup changes what gets announced, not how the component behaves. You still have to build the interaction yourself:
- Focus: The trigger can be reached with the Tab key.
- Keyboard: Enter, Space, or an arrow key opens the popup.
- Focus management: Focus moves into the popup when it opens and returns to the trigger when it closes.
None of that comes free with the attribute. aria-haspopup only indicates the availability and type of interactive popup element; it doesn't build the interaction itself.
The same property shows up on more than just stand-alone buttons. A parent menu item in a menubar component typically carries aria-haspopup="menu" when it opens a nested submenu, and a tab in a tab list can carry it too if activating that tab reveals a popup menu rather than a standard panel.
In every case, the rule is the same: whatever element actually triggers the popup is the one that gets the attribute, not a wrapping container around it.
aria-haspopup example
A menu button is the most common pattern. The trigger carries aria-haspopup="menu" alongside aria-expanded, and the popup itself uses role="menu":
<button id="actions-button" aria-haspopup="menu" aria-expanded="false" aria-controls="actions-menu">
Actions
</button>
<ul role="menu" id="actions-menu" aria-labelledby="actions-button" hidden>
<li role="menuitem" tabindex="-1">Edit</li>
<li role="menuitem" tabindex="-1">Duplicate</li>
<li role="menuitem" tabindex="-1">Delete</li>
</ul>When the button is activated, your script needs to remove the hidden attribute, move focus to the first menu item, and set aria-expanded to true – the markup doesn’t do any of that on its own.
Before the menu opens, screen readers announce the button as a collapsed menu button, so the user knows what to expect.
A dialog trigger follows the same pattern with a different value. Setting aria-haspopup="dialog" tells assistive technology users the popup is a dialog, not a menu:
<button aria-haspopup="dialog" aria-controls="confirm-dialog">
Delete project
</button>
<div role="dialog" id="confirm-dialog" aria-modal="true" aria-labelledby="dialog-title" hidden>
<h2 id="dialog-title">Delete this project?</h2>
<p>This action can't be undone.</p>
<button>Cancel</button>
<button>Delete</button>
</div>Screen readers announce the pop-up type, though each one words it differently: JAWS says the button "has popup dialog," NVDA says it "opens dialog," and VoiceOver calls it a "dialog pop-up button." Either way, the user expects a focused confirmation rather than a list of options.
A combobox doesn't need the attribute written out at all, since the implicit value already matches its popup:
<label for="city-input">City</label>
<input id="city-input" type="text" role="combobox" aria-expanded="false" aria-controls="city-listbox" aria-autocomplete="list">
<ul role="listbox" id="city-listbox" aria-label="Cities" hidden>
<li role="option">Chicago</li>
<li role="option">Columbus</li>
<li role="option">Charlotte</li>
</ul>Here, role="combobox" already carries an implicit aria-haspopup value of listbox, so adding aria-haspopup="listbox" explicitly would be redundant. aria-expanded still has to be set and toggled by hand, since that part isn't implicit.
aria-haspopup vs. aria-expanded
aria-haspopup and aria-expanded are often used on the same element, but they communicate different things and are easy to mix up.
aria-haspopup says what kind of popup an element opens. aria-expanded says whether it’s open right now.
A well-built menu button sets both: aria-haspopup="menu" so assistive technology users know what kind of popup to expect, and aria-expanded, starting at false, so they also know whether it's currently open. Leave aria-expanded off and the popup type is announced correctly, but a screen reader user has no way to tell whether that menu is open or closed without reaching inside it.
Common mistakes or misconceptions
- Mismatching
aria-haspopupwith the popup's actual role. Settingaria-haspopup="menu"on a trigger whose popup container is actuallyrole="dialog"tells assistive technology users to expect one type of interactive popup element while a different one opens. - Treating
aria-haspopupas a complete accessibility fix. The attribute only describes the popup. It doesn't add focus management, a keyboard mechanism, or any other behavior – that still has to be built separately. - Leaving
aria-expandedoff entirely. Without it, screen reader users know what kind of popup an element opens but not whether that popup is currently open or collapsed. - Assuming
aria-haspopup="true"behaves differently fromaria-haspopup="menu". They're treated identically;trueis the same asmenuin this case. - Adding
aria-haspopupto a tooltip trigger. Tooltips aren't interactive, so they don't count as popups under this attribute, andaria-haspopuphas no effect there. - Setting
aria-haspopup="listbox"explicitly on a combobox. Asrole="combobox"already carries an implicitaria-haspopupvalue oflistbox, adding it by hand is unnecessary unless the combobox opens a different kind of popup. - Putting the attribute on the wrong element.
aria-haspopupbelongs on whichever element actually triggers the popup, such as thebuttonelement or the parentmenuitem, not on a wrapping div or the popup container itself.
Frequently Asked Questions
Does aria-haspopup improve screen reader dropdown usability?
Partly. It tells screen reader users which type of popup a dropdown opens, so they know what to expect. Full usability also depends on proper keyboard support, focus management inside the popup, and pairing it with aria-expanded so users know when the dropdown is actually open.
Can aria-haspopup replace aria-expanded?
No. They cover different information. aria-haspopup is static and indicates the availability and type of interactive popup; aria-expanded is dynamic and reports whether that popup is currently open. Dropdown and menu patterns generally need both on the same trigger element.
Does a combobox need its own aria-haspopup attribute?
Not necessarily. Elements with role="combobox" have an implicit aria-haspopup value of listbox by default, since a combobox's popup is a listbox of options. Add the attribute explicitly only if the combobox opens a different type of popup, such as a dialog or a tree.
What happens if aria-haspopup and the popup's role don't match?
Assistive technology support varies depending on the browser and screen reader, but the mismatch generally confuses the announcement: a user may be told to expect a menu, grid, or listbox, and then land in a popup with a completely different role once it opens.
Does aria-haspopup work the same way across all browsers and screen readers?
Not identically. Modern browsers expose the attribute, and JAWS and NVDA have announced every popup type on buttons and links since their 2023 releases. Wording still differs between screen readers, and a11ysupport.io rates overall support as partial. Testing your menu, listbox, or dialog with at least one real screen reader is the only reliable way to confirm the announcement.
Try our website monitor free
Automate website monitoring tasks
and keep your pages compliant