skip to content

In ARIA, how do the properties aria-haspopup, aria-controls and aria-owns differ in what they tell assistive technology?

level: middleimportance: should knowfreq 36%

answer

  1. one warns, one points, one re-parents
  2. no id in the warning one
  3. true means menu, which surprises people
  4. only one owner per element
  5. fix the DOM before reaching for the heavy tool

basics

~20 s

aria-haspopup says a control opens a popup and of what type; aria-controls names, by id, the element this control governs; aria-owns re-parents elements in the accessibility tree so they become children of this element despite the DOM saying otherwise.

solid answer

~50 s

All three are relationship properties, but they carry different weight. `aria-haspopup` is a hint on the trigger: activating me opens a popup of this kind — its values are `menu`, `listbox`, `tree`, `grid`, `dialog`, plus `true` (equivalent to `menu`) and the default `false`. It names no target and says nothing about whether the popup is open; that is `aria-expanded`'s job. `aria-controls` takes an ID reference list and records which element this control governs — a tab pointing at its tabpanel, for instance. It is purely informational, and screen reader support for exposing it is uneven, so never make it load-bearing. `aria-owns` is the heavy one: it actually restructures the accessibility tree, making the referenced elements children of this element when the DOM cannot nest them. It is expensive, an element can have only one owner, and the right first move is usually to fix the DOM instead.

go deeper

for a junior

Recall the one-line role of each: haspopup warns that a popup will open, controls names the element this one governs by id, and owns re-parents elements in the accessibility tree.

for a middle

Explain that aria-haspopup carries no id and no open/closed information, that aria-controls is informational with uneven support, and that aria-owns is a tree restructuring with real constraints.

for a senior

Show judgment about aria-owns: name the single-owner rule and the tab-order mismatch, and say when you would restructure the DOM or render the popup in place instead of reaching for it.

for a principal

Own the tradeoff between portalled overlays and a coherent accessibility tree, and be able to argue what a component library should guarantee so id references never dangle across the codebase.

## Three ways to say "these things are related" Rich widgets are rarely one element. A menu button and its menu, a tab and its panel, a combobox and its popup listbox — the relationship is obvious visually and invisible in the accessibility tree unless you state it. ARIA offers three properties for stating it, and they differ sharply in what they actually do. ## aria-haspopup — an advance warning on the trigger ```html <button aria-haspopup="menu" aria-expanded="false">Actions</button> ``` This says: I am a control that opens a popup, and the popup is a menu. Screen readers typically append something like "has menu" to the button's announcement, so the user knows before activating that content will appear rather than the page navigating. The valid values are the tokens `false` (the default), `true`, `menu`, `listbox`, `tree`, `grid` and `dialog`. `true` is defined as equivalent to `menu`, which is a trap: if your button opens a dialog and you write `aria-haspopup="true"`, you have announced a menu. Write the specific token. Two things `aria-haspopup` does *not* do. It does not identify which element is the popup — there is no id in it. And it does not track open/closed; a button carries `aria-haspopup="menu"` permanently while `aria-expanded` flips between `"true"` and `"false"`. That is the state-versus-property split in miniature: one describes a fixed characteristic, the other a changing condition. ## aria-controls — a pointer, not a restructuring `aria-controls` takes a space-separated list of `id` values naming the elements this one governs: ```html <button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">Overview</button> <div role="tabpanel" id="panel-1" aria-labelledby="tab-1">…</div> ``` The relationship becomes part of the accessibility tree, and some screen readers offer a shortcut to jump to the controlled element. Support is inconsistent, however — several major screen readers expose it weakly or not at all — so the settled advice is to treat it as supplementary. If the only way a user can discover something is via `aria-controls`, the design has a hole. It is required by some role contracts (a `tabpanel` relationship, a `combobox` pointing at its popup), which is a different matter: there it is part of satisfying the role, not a substitute for a usable structure. One practical rule: every id in the list must exist in the same document at the time it is read. A dangling reference is silently useless, which happens constantly with popups that are only inserted into the DOM when they open. Either render the target up front or set the reference when you insert it. ## aria-owns — genuinely moving nodes in the tree `aria-owns` also takes ID references, but it does something categorically different: it makes the referenced elements **children of this element in the accessibility tree**, regardless of where they sit in the DOM. Use it when the required parent-child relationship cannot be expressed in the DOM — the classic case is a popup rendered at the end of `<body>` (to escape clipping or stacking problems) while its owning control lives deep inside the page. The caveats are real: - An element can be owned by **only one** element. Two owners is invalid and the result is undefined. - The order of ids in the list determines the order of the owned children, and owned children are appended after the element's existing DOM children. - It does **not** move DOM nodes, does not change tab order, and does not change what CSS sees — screen reader navigation and keyboard order can end up disagreeing if you are careless. - Browsers must recompute a chunk of the tree, so heavy use is a known performance cost. Because of all that, the guidance is consistent: restructure the DOM if you possibly can, and reach for `aria-owns` only when you truly cannot. It is a repair tool, not a design tool. ## Choosing between them Ask what you are trying to convey. "Pressing this opens a thing of type X" is `aria-haspopup`. "This element governs that element" is `aria-controls`. "That element is logically inside this one even though the DOM disagrees" is `aria-owns`. They stack rather than compete: a menu button can carry `aria-haspopup="menu"` for the affordance, `aria-expanded` for the current state, and, if the menu is portalled elsewhere in the DOM, `aria-owns` to put it back where it belongs in the tree. And as always, an id reference that points at nothing is a silent failure — nothing warns you.

  • Why is aria-haspopup="true" a poor choice for a button that opens a modal dialog?
    Because `true` is defined as equivalent to `menu`. The button would be announced as having a menu, so the user expects a list of commands with arrow-key navigation and gets a dialog instead. The token set includes `dialog` precisely for this case, along with `listbox`, `tree` and `grid`. Always write the specific token that matches what actually opens.
  • If aria-controls has patchy screen reader support, why use it at all?
    Because some role contracts expect it — a combobox pointing at its popup, for example — and because it enriches the tree for the tools that do expose it, including automated testing. What it must not be is the only way a relationship is discoverable. Design so the widget works if the property is ignored entirely, then add it as extra information.
  • What breaks when a popup is only inserted into the DOM at open time and the trigger references it by id?
    The reference dangles while the popup does not exist, so `aria-controls` or `aria-owns` points at nothing and is silently ignored — there is no error. Either render the target element up front in a hidden state, or set the referencing attribute at the moment you insert the popup and clear it when you remove it.
  • Does aria-owns change keyboard tab order?
    No. It affects only the accessibility tree, not the DOM and not focus order. Tab order still follows DOM order and tabindex. That mismatch is the main hazard: a screen reader user may perceive the owned elements as nested inside the control while a keyboard user tabs into them from a completely different part of the page.

saying these in an interview costs you the question

  • Thinking aria-haspopup indicates whether the popup is open
  • Using aria-haspopup="true" for a dialog or listbox trigger
  • Believing aria-owns moves elements in the DOM
  • Relying on aria-controls as the only cue for a relationship
  • Pointing an ID reference at an element that does not exist yet

context