Overview
Disclosure widgets reveal and hide supplementary content. The simplest and most robust approach is the native <details>/<summary> element, which provides keyboard support and screen reader announcements with zero JavaScript. When a custom design requires more control, a <button> with aria-expanded and a toggled panel is the correct fallback — never a link or a plain <div>.
WCAG Criteria:
- 4.1.2 Name, Role, Value — the trigger must expose its role and expanded/collapsed state
- 1.3.1 Info and Relationships — the relationship between trigger and content must be programmatically determinable
Key requirements:
<details>/<summary>is the preferred approach — built-in keyboard and screen reader support with no JavaScript- Always use a
<button>for custom disclosure triggers, never a link or div - Use
aria-expandedto communicate the open/closed state - Use
aria-controlsto associate the trigger with the panel (aria-controlshas limited screen reader support but is recommended for code clarity) - Toggle the
hiddenattribute on the panel so it is fully removed from the accessibility tree when closed
Native Details/Summary
Native Details/Summary
Inaccessible
<!-- Link toggle — wrong element, no state, no keyboard semantics -->
<h3>More Information</h3>
<div id="content" style="display:none">
Our product is made from 100% recycled materials...
</div>
<a href="#" onclick="toggle(); return false">Show more</a>Live Preview
More Information
Show moreAccessible
<!-- Native details/summary — zero JS, full keyboard + SR support -->
<details>
<summary>More Information</summary>
<div>
Our product is made from 100% recycled materials...
</div>
</details>Live Preview
More Information
Our product is made from 100% recycled materials. It is designed to last for years and comes with a lifetime warranty. Contact support for more details.
What’s wrong with the link toggle?
- A link (
<a href="#">) is semantically a navigation element, not a toggle — screen readers announce it as “link” rather than a button or disclosure - No
aria-expandedstate — screen readers cannot tell whether the content is currently visible or hidden - The link text changes between “Show more” and “Show less,” which can confuse speech recognition users who need a stable name
- Using
onclickwithreturn falseon a link is a hack — keyboard users who press Enter may trigger unexpected scroll-to-top behavior - The content is hidden with
display:nonevia inline style rather than thehiddenattribute, which works but bypasses the native disclosure pattern entirely
What the screen reader announces:
| Version | Announcement |
|---|---|
Inaccessible (<a> toggle) | “Show more, link” (no expanded/collapsed state) |
Accessible (<details>) | “More Information, collapsed, summary” / “More Information, expanded, summary” |
Custom Disclosure
Custom Disclosure
Inaccessible
<!-- Div trigger — no role, no state, no keyboard access -->
<div class="trigger" onclick="togglePanel()">
FAQ Answer
</div>
<div class="panel" style="display:none">
You can cancel your subscription at any time...
</div>Live Preview
FAQ Answer
Accessible
<!-- Button with aria-expanded + hidden panel -->
<button
aria-expanded="false"
aria-controls="disclosure-good-panel"
onclick="
var expanded = this.getAttribute('aria-expanded') === 'true';
this.setAttribute('aria-expanded', String(!expanded));
var panel = document.getElementById('disclosure-good-panel');
if (!expanded) { panel.removeAttribute('hidden'); }
else { panel.setAttribute('hidden', ''); }
"
>
FAQ Answer
</button>
<div id="disclosure-good-panel" role="region" hidden>
You can cancel your subscription at any time...
</div>Live Preview
You can cancel your subscription at any time from your account settings page. Refunds are processed within 5 business days.
What’s wrong with the div trigger?
- A
<div>has no button role — screen readers do not identify it as interactive - It cannot receive keyboard focus with Tab
- It cannot be activated with Enter or Space
- No
aria-expandedmeans the open/closed state is invisible to assistive technology onclickon a div only works with a mouse
What the screen reader announces:
| Version | Announcement |
|---|---|
Inaccessible (<div>) | “FAQ Answer” (just text, no role, no state) |
Accessible (<button>) | “FAQ Answer, collapsed, button” |
| When expanded | ”FAQ Answer, expanded, button” |
When to Use Which Pattern
| Pattern | Element | JS required | Use when |
|---|---|---|---|
| Native disclosure | <details> + <summary> | No | Simple show/hide content — FAQ answers, supplementary info, expandable sections |
| Custom disclosure | <button> + aria-expanded | Yes | You need custom animation, styling, or behavior that <details> cannot support |
Key differences:
<details>/<summary>provides keyboard support, focus management, and expanded/collapsed state announcements automatically with zero JavaScript- Custom disclosure requires manually managing
aria-expanded, thehiddenattribute, and keyboard interaction — use only when you need control that native elements cannot provide aria-controlsassociates the button with its panel; while screen reader support foraria-controlsis limited, it is recommended for developer clarity and future compatibility- Never use a link (
<a>) or plain<div>as a disclosure trigger — links are for navigation, divs are not interactive