Skip to content

Clarify relationship between aria-hidden and aria-owns - #1839

Merged
spectranaut merged 24 commits into
w3c:mainfrom
adampage:owns-ignores-hidden
Sep 10, 2025
Merged

spectranaut merged 24 commits into
w3c:mainfrom
adampage:owns-ignores-hidden

Conversation

@adampage

@adampage adampage commented Oct 21, 2022 •

Copy link
Copy Markdown
Member

Closes #1818.

The definition of Owning Element is already inclusive of both DOM ancestry and ownership via aria-owns:

An 'owning element' is any DOM ancestor of the element, or any element with an aria-owns attribute which references the ID of the element.

Because of this, I’m thinking it’ll be sufficient to swap out “ancestors” for “owning elements” in aria-hidden’s description.

I also editorially removed what I believe is an unhelpful comma.

Test, Documentation and Implementation tracking

Once this PR has been reviewed and has consensus from the working group, tests should be written and issues should be opened on browsers. Add N/A and check when not applicable.


Preview | Diff

@adampage adampage self-assigned this Oct 21, 2022
@adampage adampage added the clarification clarifying or correcting language that is either confusing, misleading or under-specified label Oct 21, 2022
@adampage

Copy link
Copy Markdown
Member Author

Here’s a Codepen with a few exploratory tests.

@Jym77

Jym77 commented Nov 30, 2022

Copy link
Copy Markdown

Two points to mention:


I'm encountering a problem similar to #1818 but the other way around, and I'm not sure this PR fully handles it 🤔

The case in #1818 was:

  <ul aria-owns="li"></ul>
  <div aria-hidden="true">
    <li id="li">Child node</li>
  </div>

That is, a not-hidden element aria-owns an element with a hidden DOM ancestor. In that case, this PR says that the li#li element should also be hidden because it has an "owning element" (here, DOM ancestor) which is hidden.

What happens in the reverse case:

<div aria-hidden="true">
  <div id="owner" aria-owns="li" />
</div>
<ul> <li id="li">Item</li> </ul>

In that case, a hidden element aria-owns an element with no hidden ancestor. The clarification in this PR also means that li#li should be hidden because it has an "owning element" (here, an element with aria-owns) which is hidden.
Quick tests, it seems that browsers ignore aria-owns in this case.
So, mostly raising the point to make sure this is the intention of the PR.


Second point is that the language in §7.1 Excluding Elements from the Accessibility Tree uses a lot of "descendant" and other tree-related wording, notably for the case concerning this PR:

user agents SHOULD NOT include the following elements in the accessibility tree:

  • Elements, including their descendants, that have aria-hidden set to true.

Should there be a precision that "descendants" here mean "either descendant in the DOM tree, or descendant in the accessibility tree where aria-owns relations are taken into account"? Most of this Section would benefit from such clarification. Maybe it's a topic for another PR, though 🤔

@pkra

pkra commented Nov 30, 2022

Copy link
Copy Markdown
Member

For the second point: #1150

@adampage

adampage commented Dec 6, 2022

Copy link
Copy Markdown
Member Author

In that case, a hidden element aria-owns an element with no hidden ancestor.

Mmm, thanks for poking at this, @Jym77. I hadn’t considered that reverse case.

“Owning element” is currently defined as “any DOM ancestor of the element, or any element with an aria-owns attribute which references the ID of the element.”

In this reverse scenario, <div aria-hidden="true"> is neither a DOM ancestor of li#li nor does it use aria-owns to reference li#li directly. It feels a bit lawyerly 🤔, but I think we could then say that <div aria-hidden="true"> is not an “owning element” of li#li. div#owner is hidden because its DOM ancestor is aria-hidden, so its use of aria-owns is no longer exposed to the accessibility API. This leaves li#li in its original location, owned by its parent ul.

@Jym77

Jym77 commented Dec 7, 2022

Copy link
Copy Markdown

@adampage That actually makes sense 😄
This PR seems to change the behaviour in the related simplified case:

<div aria-hidden="true" aria-owns="li"></div>

<ul> <li id="li">Item</li> </ul>

In that case, the current behaviour is to expose li#li as it has no ancestor with aria-hidden (and Chrome and FF do that), but the new behaviour will be to hide it since it has a owning element with aria-hidden.

I guess it's a bit of a question which of aria-owns or aria-hidden "acts faster". The current behaviour is that aria-hidden hides away the owner before it has time to own anything else, but the new behaviour is that aria-owns is tweaking the tree before the owner gets hidden 🤔


Can get weirder with a case like:

<div aria-hidden="true" aria-owns="foo"></div>
<div id="foo" aria-owns="bar"></div>
<div id="bar"></div>

Now, #foo has a owning element with aria-hidden, so it is hidden. But since it itself does not have aria-hidden, it still owns #bar, which is also not hidden in any way. I feel that is at best confusing and might explode in weird ways 🤔

@adampage

adampage commented Dec 7, 2022

Copy link
Copy Markdown
Member Author

This discussion has been super helpful, @Jym77, thank you. 😁 I think you hit the nail on the head with the question of which attribute “acts faster”.

Let’s say that this PR now includes some additional text (somewhere appropriate in the spec) to the effect of:

“Changes to ownership are resolved in DOM order. If both aria-hidden and aria-owns are specified on the same element, aria-hidden is processed first and aria-owns will not take effect.”

Now let’s apply that to the four scenarios that have come up. In the initial #1818 example:

  <ul aria-owns="li"></ul>
  <div aria-hidden="true">
    <li id="li">Child node</li>
  </div>

...the ul steals ownership of #li from div[aria-hidden="true"] because it’s encountered first in DOM order. #li becomes not hidden because ul is not hidden. By the time div[aria-hidden="true"] is encountered, it no longer has a descendant to be affected by its use of aria-hidden. It hides itself, full stop.

In the reverse case:

<div aria-hidden="true">
  <div id="owner" aria-owns="li" />
</div>
<ul> <li id="li">Item</li> </ul>

...div[aria-hidden="true"] is encountered first, so it excludes itself and its descendants from the accessibility API, leaving #owner[aria-owns="li"] altogether ignored. #li continues to be naturally owned by ul and not hidden.

In the follow-up simplified case:

<div aria-hidden="true" aria-owns="li"></div>
<ul> <li id="li">Item</li> </ul>

...div[aria-hidden="true"] is encountered, and it resolves aria-hidden before aria-owns. This prevents the change in ownership of #li. #li continues to be naturally owned by ul, and not hidden.

In the follow-up weirder case:

<div aria-hidden="true" aria-owns="foo"></div>
<div id="foo" aria-owns="bar"></div>
<div id="bar"></div>

...the previous story is essentially true again. div[aria-hidden="true"] is encountered, and it resolves aria-hidden before aria-owns. This prevents the change in ownership for #foo. #foo is then encountered, but is not hidden so its ownership of #bar is resolved and both remain not hidden.


The original intention of this spec clarification was to align with the current Chrome & Firefox behavior, so I built out all four of these scenarios in Codepen and tested them with macOS + VoiceOver + Chrome. Each outcome I described above is correctly demonstrated by that AT combo.

Whew. Having said all that, would you agree that the gist of my additional spec proposal is helpful & accurate? And that it would reasonably lead to the interpretations I described for each of those four scenarios? Are there yet other ambiguous scenarios lurking out there? 😅

Thanks again!

@Jym77

Jym77 commented Dec 8, 2022

Copy link
Copy Markdown

“Changes to ownership are resolved in DOM order. If both aria-hidden and aria-owns are specified on the same element, aria-hidden is processed first and aria-owns will not take effect.”

Whew. Having said all that, would you agree that the gist of my additional spec proposal is helpful & accurate? And that it would reasonably lead to the interpretations I described for each of those four scenarios? Are there yet other ambiguous scenarios lurking out there? 😅

Yes, I think that clarification works. Maybe we need to add that change of ownership is only initiated by non-hidden node (that's more or less the second sentence but making it clear that one should stop looking for aria-owns upon encountering a aria-hidden, i.e. the way case 2 works)¹
From a graph-theory point of view, we'd walk the DOM tree in DOM order, stop the current traversal on aria-hidden or already traversed node, and jump to follow aria-owns (which does trigger the "already traversed node" in the previous check).

This also makes it clear that aria-hidden affects the accessibility tree while display: none affects the DOM tree (so replacing aria-hidden by display: none in case 2 does not un-hide the li, it is still not rendered, therefore hidden).

Great clarification 👍

@scottaohara

scottaohara commented Dec 8, 2022 •

Copy link
Copy Markdown
Member

was following along with this until:

This also makes it clear that aria-hidden affects the accessibility tree while display: none affects the DOM tree (so replacing aria-hidden by display: none in case 2 does not un-hide the li, it is still not rendered, therefore hidden).

this statement has made me wonder if this actually makes sense or not. since display none does modify the a11y tree... and then i'm not sure what case 2 refers to? the second of Adam's examples in his last comment, or the second odd case you brought up, @Jym77 ?

@Jym77

Jym77 commented Dec 8, 2022

Copy link
Copy Markdown

Yep, poor writing on my side 🙈 and messing up things badly…

  <ul aria-owns="li"></ul>
  <div style="display: none">
    <li id="li">Child node</li>
  </div>

case 1 not 2 in Adam's list with aria-hidden replaced by display: none.

This does not expose li#li since it hits the "Elements, including their descendent elements, that have host language semantics specifying that the element is not displayed" condition.

That is of course linked to non-interference with the host language (aria-owns cannot expose an element that HTML is hiding, but can expose an element that ARIA is trying to hide (aria-hidden)). Which is more or less what I was trying to say by putting display: none at the DOM level and aria-hidden at the accessibility tree level.
Of course, display: none affects the accessibility tree, but indirectly because it acts on the DOM tree in way that accessibility tree building is concerned, that's why I was writing that it affects the DOM tree.

@scottaohara

Copy link
Copy Markdown
Member

Thanks for the correction/clarification. Yah that makes sense again now.

@adampage
adampage force-pushed the owns-ignores-hidden branch from 3ddc773 to 4d5c3a6 Compare January 11, 2023 02:43
@adampage

Copy link
Copy Markdown
Member Author

So, I reacquainted myself with the spec’s definition of “hidden”:

Indicates that the element is not visible, perceivable, or interactive to any user. An element is considered hidden if it or any one of its ancestor elements is not rendered or is explicitly hidden.

Based on that, I took another pass at the aria-hidden section with the goal of disambiguating its use of the term “hidden” by leveraging the existing “Excluding Elements from the Accessibility Tree” section.

Beyond that, I wordsmithed the clarifications we discussed earlier in this thread and added them to the aria-owns section.

@Jym77

Jym77 commented Jan 12, 2023

Copy link
Copy Markdown

I feel there is still a small clarification needed to say that aria-owns breaks DOM parent/child relation in addition to create new ones (i.e. the result is indeed an accessibility tree, not a DAG).

Owning element states:

An 'owning element' is any DOM ancestor of the element, or any element with an aria-owns attribute which references the ID of the element.

Because of the "or", and no mention of unicity, in case 1 above

<ul aria-owns="li"></ul>
  <div aria-hidden="true">
    <li id="li">Child node</li>
  </div>

Both the ul and the div are "owning element" for the li (or at least the definition doesn't really tells which to use), and the definition of aria-owns doesn't really answer that question either. (and since any DOM ancestor is a "owning element", it is actually not a problem to have more than one owning element).

So, without disambiguating that, the li has an aria-hidden "owning element" (the div) and should be hidden, which is precisely what we want to avoid.

Should we change the definition of "owning element" to state something like

An 'owning element' is any element with an aria-owns attribute which references the ID of the element or, if none, any DOM ancestor of the element.

Otherwise, I think this is correct and expresses what is needed 👍

@spectranaut

Copy link
Copy Markdown
Contributor

I just want to note that @Jym77's recommended definition of owned is nice, and, that there is also a 1.3-blocking issue related to inconsistent use the term "owned by", just like how hidden was just revisited:

#1161

The "owned by inconsistencies" can be addressed before of after this PR, but it might also lead to a term change or term update.

@adampage

adampage commented May 9, 2025

Copy link
Copy Markdown
Member Author

Thanks very much, @scottaohara! I committed your example and then made a couple follow-on editorial tweaks.

Does this read okay to you?:

In the following example, <div id="instructions"> and its text content remain exposed and unmoved in the accessibility tree since the element attempting to reparent it with aria-owns is also hidden from all users.

@scottaohara

Copy link
Copy Markdown
Member

i'm going to approve as is - but i don't think "also" needed to be added. that made me initially think that the sentence was trying to say that the element that was attempting to be reparented was hidden from the a11y tree - and it made me go back to look if that had been changed

@adampage

Copy link
Copy Markdown
Member Author

Updated. Thank you, @scottaohara. 🙏🏻

@pkra pkra added the waiting for implementations Cannot be merged until there are two browser impls or one impl + impl commit label May 27, 2025
@pkra

pkra commented May 27, 2025

Copy link
Copy Markdown
Member

Tentatively labeling this "waiting for implementations" since the thread on WPT leaves me wondering if implementation work is needed after all. Please correct me if I'm getting it wrong (e.g., merge the PR).

@spectranaut

Copy link
Copy Markdown
Contributor

Agenda-ing this also for the tests. We should get this landed, and the tests, if we want to move forward with this change.

@adampage

adampage commented Sep 4, 2025

Copy link
Copy Markdown
Member Author

Here are the latest browser results for the companion WPT test.

@spectranaut

Copy link
Copy Markdown
Contributor

Just read the minutes from yesterday's meeting: https://www.w3.org/2025/09/04-aria-minutes.html#9476

Since this is largely in agreement with two implementations (besides minor bugs) I think we should land this change THEN open a bug on webkit and everyone else.

@spectranaut

Copy link
Copy Markdown
Contributor

Actually, I said this is ready for merge, but it would be nice to get an approving review from @cookiecrook first

@cookiecrook cookiecrook left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving with one editorial clarification.

Comment thread index.html Outdated
adampage and others added 2 commits September 9, 2025 15:28
Co-authored-by: James Craig <cookiecrook@users.noreply.github.com>
@adampage

adampage commented Sep 9, 2025

Copy link
Copy Markdown
Member Author

Thanks, @cookiecrook. I’ve committed that suggestion and merged in the latest, @spectranaut 🚀

@spectranaut spectranaut moved this from Needs Review to Merge? in ARIA Normative PR Tracking Sep 10, 2025
@spectranaut
spectranaut merged commit 31766e3 into w3c:main Sep 10, 2025
7 checks passed
@github-project-automation github-project-automation Bot moved this from Merge? to Waiting For Implementation in ARIA Normative PR Tracking Sep 10, 2025
github-actions Bot added a commit that referenced this pull request Sep 10, 2025
SHA: 31766e3
Reason: push, by spectranaut

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@spectranaut spectranaut removed Agenda waiting for implementations Cannot be merged until there are two browser impls or one impl + impl commit labels Sep 10, 2025
beckysiegel pushed a commit to chromium/chromium that referenced this pull request Oct 22, 2026
Per the ARIA spec, "User agents MUST NOT resolve aria-owns when it is
set on an element that has been excluded from the accessibility tree"
[1]. AXRelationCache::IsValidOwner() already rejected aria-hidden
owners, but not owners that are display:none (e.g. via the hidden
attribute), visibility:hidden, or inside such an ancestor. As a
result, a hidden element could pull a visible element out of its
natural parent and park it under an invisible, ignored subtree, which
among other things emptied the natural parent's name-from-contents.

This made Chromium fail two subtests of
external/wpt/accname/aria-owns.html that Firefox and Safari pass:
* Ignore aria-owns when on an element that is hidden from all users
* Computed name of parent heading persists when aria-owns fails to
  relocate its contents

Note that this deliberately reverses behavior introduced in
https://crrev.com/c/2575445 (crbug.com/1155617), which ensured that
invisible aria-owns sources are included in the tree so that they
could own visible children. That predates the spec text above (added
in w3c/aria#1839). Elements with aria-owns are still force-included in
the tree so that their relation bookkeeping can be set up and torn
down; they just no longer get to own anything while hidden.

This CL:
- Rejects owners for which IsHiddenViaStyle() is true, alongside the
  existing IsAriaHidden() check. Owned children were already required
  to have a layout object, so this only closes the gap on the owner
  side.
- Re-validates aria-owns when the visibility of an element with
  aria-owns changes. Unlike display:none and aria-hidden changes, a
  visibility change does not rebuild the AXObject (which would re-queue
  the owner), so without this an owner that became visible again would
  never re-establish its relation. Dropping the relation when the owner
  becomes hidden already happens via ValidatedAriaOwner() when the
  owner's children are rebuilt.
- Rebaselines aria-owns-from-display-none, which encoded the old
  behavior, and adds aria-owns-from-hidden-owner (static scenarios) and
  accessibility/aria-owns-hidden-owner.html (static + dynamic
  scenarios, including hidden/visibility round trips).
- Removes the WPT expectation file for accname/aria-owns.html, which
  now passes entirely.

Related: crbug.com/409069366 (re-queue aria-owns when an AXObject is
recreated) is why the display:none round trip already worked and only
the visibility round trip needed the new hook. crbug.com/503872382
(ValidatedAriaOwner() restores the natural parent when an owner becomes
invalid) is the cleanup path this change relies on when an owner is
hidden.

Known limitation: the AX cache only learns about visibility changes via
LayoutObject::StyleDidChange(), so an owner that is display:contents is
not re-validated when its visibility is toggled at runtime. Static
display:contents owners (visible or hidden) are handled correctly. This
is a pre-existing gap in the style-change plumbing; a complete fix would
need a notification from Element::RecalcOwnStyle(). Not addressed here
since the combination is rare and nothing gets worse than before.

Out of scope: spec rule 2 also says that a visibility:hidden *target*
must not be owned; Chromium currently keeps such a target owned but
ignored. The WPT does not test this; left for a follow-up.

[1] https://w3c.github.io/aria/#aria-owns

The new dump test is not registered in the Android
WebContentsAccessibilityTreeTest, as its Android expectations cannot be
generated without an Android build; happy to add them if a reviewer
can supply the output.

R=aleventhal@chromium.org, dtseng@chromium.org

ARIA spec and Firefox/Safari. The referenced elements stay with their
natural parent.

AX-Relnotes: aria-owns is now ignored on elements that are hidden via
display: none, the hidden attribute, or visibility:hidden, matching the
Bug: 574690367
Change-Id: I6854152ae48fe67a0099ab15ce46ec1f3b9e2707
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8603287
Commit-Queue: Sarah Drasner <sdrasner@google.com>
Reviewed-by: David Tseng <dtseng@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1724275}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clarification clarifying or correcting language that is either confusing, misleading or under-specified spec:aria

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Should aria-hidden be affected by aria-owns?

7 participants