Don't use aria-haspopup

Reasons to avoid using the aria-haspopup property.

Let me clarify, don’t use aria-haspopup unless it triggers a menu.

aria-haspopup is an attribute that indicates if an element can trigger a popup. Version 1.0 of Aria supported the values true and false. As of version 1.1 it accepts a value of:

The popup element must have a role that matches the value of aria-haspopup. A common case where you might be tempted to use this attribute is if you have a button that triggers a dialog:

<button aria-haspopup="dialog">Action</button>
<div role="dialog">
  // dialog content
<div>

The problem

Support for these new values is awfully bad, with the exception of Voiceover in macOS and IOS.

In Voiceover the following example using aria-haspopup="dialog" will be announced as:

Action, popup button

<button aria-haspopup="dialog">Action</button>

Unfortunately, not every screen reader where this isn’t supported will ignore it. In some cases it will fallback to the value of menu.

This is how some popular screen readers will anounce it.

NVDA

JAWS

An attribute is also a promise

Principle 1 of Aria: a role is a promise. Which means you are making a promise that the usage of the element with a role will have the expected keyboard interactions.

I’d argue the same can be said when using an attribute. If a link in a main navigation has aria-current="page", the user expects to be in the current page of the site. If a button says it triggers a menu, the user will expect a menu, with the interactions expected from a menu. If it instead opens a dialog, that’s a bad and confusing experience.

So even though it is part of the spec, it is on us to test and make sure we are providing a good experience.

Alternatives

So how do you convey that a button opens a dialog?

Beware of aria-label

If you are using text as an accessible name in a button, aria-label will override the text inside.

<button aria-label="opens dialog">Action</button>

Will be read as opens dialog

It will also result in a failure of 2.5.3 - Label in name. Which states the the visual name should be the same or match the programmatic name.

Visually hiding text

You could use a span inside the button that is visually hidden. If validation and following specs is your thing, don’t use a div inside a button, as it expects phrasing content.

<button>
  <span class="visually-hidden">Opens dialog</span>
  Action
</button>
<div role="dialog">
  // dialog content
<div>
.visually-hidden {
  clip: rect(0 0 0 0); 
  clip-path: inset(100%); 
  width: 1px;
  height: 1px; 
  overflow: hidden; 
  position: absolute; 
  white-space: nowrap; 
}

This will hide the text visually, but be read by screen readers.

Icon only button

Using text as an accessible name in a button isn’t always an option. You may be requested to use an icon button with no text. In this case you could again use a visually hidden span inside the button.

<button>
  <span class="visually-hidden">Opens dialog, action</span>
  <svg aria-hidden="true"></svg>
</button>
<div role="dialog">
  // dialog content
<div>

TL;DR

Support for aria-haspopup is currently not good enough to be used, unless you are triggering a menu. NVDA and JAWS wil wrongly announce other values as a menu, which is no bueno.