Provide a general summary of the issue here
useDisclosure returns 'aria-hidden': !state.isExpanded in panelProps, while its layout effect sets hidden="until-found" on the same element when the panel is collapsed. HTML-ARIA has a normative prohibition on that exact pairing, added in w3c/html-aria#507:
Authors SHOULD NOT use the aria-hidden="true" attribute on any element which also has a hidden attribute.
Authors MUST NOT use aria-hidden="true" on any element which also has the hidden attribute specified in the until-found state.
The Nu HTML Checker flags this, and so do the accessibility scanners built on it — we hit it on an enterprise audit (SortSite rule W3cHtml5AriaHiddenNotAllowed-hidden-until-found, reported against WCAG 4.1.2 / Section 508 4.1.2).
🤔 Expected Behavior?
A collapsed DisclosurePanel carries hidden="until-found" and no aria-hidden. hidden in any state already removes the subtree from the accessibility tree, so aria-hidden adds nothing that isn't already true.
😯 Current Behavior
A collapsed panel renders both:
<div id="react-aria-3" role="group" aria-labelledby="react-aria-2"
hidden="until-found" aria-hidden="true" data-rac="">…</div>
Source, packages/react-aria/src/disclosure/useDisclosure.ts on main:
panelProps: {
id: panelId,
// This can be overridden at the panel element level.
role: 'group',
'aria-labelledby': triggerId,
'aria-hidden': !state.isExpanded, // ← line 163
hidden: isSSR ? !state.isExpanded : undefined
}
Two smaller consequences of the same line:
- During the collapse animation the panel is still visible but already
aria-hidden="true". The hidden attribute is deliberately deferred until panel.getAnimations() resolve (line ~124), but aria-hidden flips synchronously with the state. For the duration of the transition, on-screen content is hidden from assistive technology.
aria-hidden="false" on the expanded panel. APG discourages the "false" value — it can't force exposure if an ancestor is hidden, so it reads as a guarantee it doesn't provide.
To be clear about what is not broken: the beforematch handler works. When find-in-page reveals the panel, flushSync(() => state.toggle()) clears aria-hidden in the same commit, so the "content stays hidden from AT after being found" failure the spec warns about doesn't actually reproduce. The issue is conformance and the two side effects above, not a dead end for screen reader users.
💁 Possible Solution
Drop the aria-hidden entry from panelProps. The hidden prop on the line below already covers the SSR and no-until-found-support paths, and the layout effect covers the client path:
panelProps: {
id: panelId,
// This can be overridden at the panel element level.
role: 'group',
'aria-labelledby': triggerId,
- 'aria-hidden': !state.isExpanded,
hidden: isSSR ? !state.isExpanded : undefined
}
🔦 Context
We use Disclosure for the accordion component in an enterprise application that is contractually audited for WCAG 2.2 AA and Section 508. The finding is raised on every page containing an accordion, and the audit tooling isn't ours to configure, so a conformance error can't be dismissed as a false positive on our side.
DisclosurePanel merges as mergeProps(DOMProps, renderProps, panelProps, focusWithinProps), so panelProps wins and the attribute can't be overridden from userland. Our workaround is a layout effect that strips it, which needs a DisclosureStateContext subscription because DisclosurePanel re-renders as a context consumer while the parent bails out:
const panelRef = useRef<HTMLDivElement>(null);
const state = useContext(DisclosureStateContext);
useLayoutEffect(() => {
panelRef.current?.removeAttribute('aria-hidden');
}, [state?.isExpanded]);
return <DisclosurePanel ref={panelRef}>{children}</DisclosurePanel>;
Happy to open a PR for the one-line removal if you'd like.
🖥️ Steps to Reproduce
import {Button, Disclosure, DisclosurePanel, Heading} from 'react-aria-components';
<Disclosure>
<Heading><Button slot="trigger">Trigger</Button></Heading>
<DisclosurePanel>Panel content</DisclosurePanel>
</Disclosure>
- Render the above in Chrome (or any browser supporting
hidden=until-found) and leave it collapsed.
- Inspect the panel element — it has both
hidden="until-found" and aria-hidden="true".
- Run the page through the Nu HTML Checker (validator.w3.org/nu) to see the conformance error.
Version
react-aria-components 1.18.0 / react-aria 3.49.0. Still present on main at packages/react-aria/src/disclosure/useDisclosure.ts:163.
What browsers are you seeing the problem on?
Chrome
If other, please specify.
No response
What operating system are you using?
MacOS
🧢 Your Company/Team
No response
🕷 Tracking Issue
No response
Provide a general summary of the issue here
useDisclosurereturns'aria-hidden': !state.isExpandedinpanelProps, while its layout effect setshidden="until-found"on the same element when the panel is collapsed. HTML-ARIA has a normative prohibition on that exact pairing, added in w3c/html-aria#507:The Nu HTML Checker flags this, and so do the accessibility scanners built on it — we hit it on an enterprise audit (SortSite rule
W3cHtml5AriaHiddenNotAllowed-hidden-until-found, reported against WCAG 4.1.2 / Section 508 4.1.2).🤔 Expected Behavior?
A collapsed
DisclosurePanelcarrieshidden="until-found"and noaria-hidden.hiddenin any state already removes the subtree from the accessibility tree, soaria-hiddenadds nothing that isn't already true.😯 Current Behavior
A collapsed panel renders both:
Source,
packages/react-aria/src/disclosure/useDisclosure.tsonmain:Two smaller consequences of the same line:
aria-hidden="true". Thehiddenattribute is deliberately deferred untilpanel.getAnimations()resolve (line ~124), butaria-hiddenflips synchronously with the state. For the duration of the transition, on-screen content is hidden from assistive technology.aria-hidden="false"on the expanded panel. APG discourages the"false"value — it can't force exposure if an ancestor is hidden, so it reads as a guarantee it doesn't provide.To be clear about what is not broken: the
beforematchhandler works. When find-in-page reveals the panel,flushSync(() => state.toggle())clearsaria-hiddenin the same commit, so the "content stays hidden from AT after being found" failure the spec warns about doesn't actually reproduce. The issue is conformance and the two side effects above, not a dead end for screen reader users.💁 Possible Solution
Drop the
aria-hiddenentry frompanelProps. Thehiddenprop on the line below already covers the SSR and no-until-found-support paths, and the layout effect covers the client path:panelProps: { id: panelId, // This can be overridden at the panel element level. role: 'group', 'aria-labelledby': triggerId, - 'aria-hidden': !state.isExpanded, hidden: isSSR ? !state.isExpanded : undefined }🔦 Context
We use
Disclosurefor the accordion component in an enterprise application that is contractually audited for WCAG 2.2 AA and Section 508. The finding is raised on every page containing an accordion, and the audit tooling isn't ours to configure, so a conformance error can't be dismissed as a false positive on our side.DisclosurePanelmerges asmergeProps(DOMProps, renderProps, panelProps, focusWithinProps), sopanelPropswins and the attribute can't be overridden from userland. Our workaround is a layout effect that strips it, which needs aDisclosureStateContextsubscription becauseDisclosurePanelre-renders as a context consumer while the parent bails out:Happy to open a PR for the one-line removal if you'd like.
🖥️ Steps to Reproduce
hidden=until-found) and leave it collapsed.hidden="until-found"andaria-hidden="true".Version
react-aria-components1.18.0 /react-aria3.49.0. Still present onmainatpackages/react-aria/src/disclosure/useDisclosure.ts:163.What browsers are you seeing the problem on?
Chrome
If other, please specify.
No response
What operating system are you using?
MacOS
🧢 Your Company/Team
No response
🕷 Tracking Issue
No response