Disclosure
A button that controls the visibility of a section of content.
🤖 AI Implementation GuideDemo
Basic Disclosure
A simple disclosure that toggles the visibility of content when clicked.
Initially Expanded
Use the defaultExpanded prop to show the content on initial render.
This content is visible when the page loads because defaultExpanded is set to
true.
Disabled State
Use the disabled prop to prevent interaction with the disclosure.
Native HTML
Use Native HTML First
Before using this custom component, consider using native <details> and <summary> elements.
They provide built-in accessibility, work without JavaScript, and require no ARIA attributes.
<details>
<summary>Show details</summary>
<p>Hidden content here...</p>
</details> Use custom implementations only when you need smooth height animations, external state control, or styling that native elements cannot provide.
| Use Case | Native HTML | Custom Implementation |
|---|---|---|
| Simple toggle content | Recommended | Not needed |
| JavaScript disabled support | Works natively | Requires fallback |
| Smooth animations | Limited support | Full control |
| External state control | Limited | Full control |
| Custom styling | Browser-dependent | Full control |
Accessibility Features
WAI-ARIA Roles
| Role | Target Element | Description |
|---|---|---|
button | Trigger element | Interactive element that toggles panel visibility (use native <button>) |
WAI-ARIA Disclosure Pattern (opens in new tab)
WAI-ARIA Properties
| Attribute | Target | Values | Required | Description |
|---|---|---|---|---|
aria-controls | button | ID reference to panel | Yes | Associates the button with the panel it controls |
aria-hidden | panel | true | false | No | Hides panel from assistive technology when collapsed |
WAI-ARIA States
aria-expanded
Indicates whether the disclosure panel is expanded or collapsed.
| Target | button element |
| Values | true | false |
| Required | Yes |
| Change Trigger | Click, Enter, Space |
| Reference | aria-expanded (opens in new tab) |
Keyboard Support
| Key | Action |
|---|---|
| Tab | Move focus to the disclosure button |
| Space / Enter | Toggle the visibility of the disclosure panel |
Disclosure uses native <button> element behavior for keyboard interaction. No additional
keyboard handlers are required.
Source Code
---
/**
* APG Disclosure Pattern - Astro Implementation
*
* A button that controls the visibility of a section of content.
* Uses Web Components for client-side state management.
*
* @see https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/
*/
export interface Props {
/** Content displayed in the disclosure trigger button */
trigger: string;
/** When true, the panel is expanded on initial render */
defaultExpanded?: boolean;
/** When true, the disclosure cannot be expanded/collapsed */
disabled?: boolean;
/** Additional CSS class */
class?: string;
}
const { trigger, defaultExpanded = false, disabled = false, class: className = '' } = Astro.props;
// Generate unique ID for this instance
const instanceId = crypto.randomUUID();
const panelId = `${instanceId}-panel`;
---
<apg-disclosure class:list={['apg-disclosure', className]} data-expanded={defaultExpanded}>
<button
type="button"
aria-expanded={defaultExpanded}
aria-controls={panelId}
disabled={disabled}
class="apg-disclosure-trigger"
>
<span class="apg-disclosure-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 6 15 12 9 18"></polyline>
</svg>
</span>
<span class="apg-disclosure-trigger-content">{trigger}</span>
</button>
<div
id={panelId}
class="apg-disclosure-panel"
aria-hidden={!defaultExpanded}
inert={!defaultExpanded ? true : undefined}
>
<div class="apg-disclosure-panel-content">
<slot />
</div>
</div>
</apg-disclosure>
<script>
class ApgDisclosure extends HTMLElement {
private button: HTMLButtonElement | null = null;
private panel: HTMLElement | null = null;
private expanded = false;
private rafId: number | null = null;
connectedCallback() {
// Use requestAnimationFrame to ensure DOM is fully constructed
this.rafId = requestAnimationFrame(() => this.initialize());
}
private initialize() {
this.rafId = null;
this.button = this.querySelector('.apg-disclosure-trigger');
this.panel = this.querySelector('.apg-disclosure-panel');
if (!this.button || !this.panel) {
console.warn('apg-disclosure: button or panel not found');
return;
}
this.expanded = this.dataset.expanded === 'true';
// Attach event listener
this.button.addEventListener('click', this.handleClick);
}
disconnectedCallback() {
// Cancel pending initialization
if (this.rafId !== null) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
// Remove event listener
this.button?.removeEventListener('click', this.handleClick);
// Clean up references
this.button = null;
this.panel = null;
}
private toggle() {
this.expanded = !this.expanded;
this.updateDOM();
// Dispatch custom event
this.dispatchEvent(
new CustomEvent('expandedchange', {
detail: { expanded: this.expanded },
bubbles: true,
})
);
}
private updateDOM() {
if (!this.button || !this.panel) return;
// Update button aria-expanded
this.button.setAttribute('aria-expanded', String(this.expanded));
// Update panel aria-hidden
this.panel.setAttribute('aria-hidden', String(!this.expanded));
// Toggle inert attribute
if (this.expanded) {
this.panel.removeAttribute('inert');
} else {
this.panel.setAttribute('inert', '');
}
}
private handleClick = () => {
if (this.button?.disabled) return;
this.toggle();
};
}
// Register the custom element
if (!customElements.get('apg-disclosure')) {
customElements.define('apg-disclosure', ApgDisclosure);
}
</script> Usage
---
import Disclosure from './Disclosure.astro';
---
<Disclosure trigger="Show details" defaultExpanded={false}>
<p>Hidden content that can be revealed</p>
</Disclosure> API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
trigger | string | required | Content displayed in the trigger button |
slot (default) | any | - | Content displayed in the panel |
defaultExpanded | boolean | false | Initially expanded state |
disabled | boolean | false | Disable the disclosure |
class | string | "" | Additional CSS class |
Custom Events
| Event | Detail | Description |
|---|---|---|
expandedchange | { expanded: boolean } | Dispatched when expanded state changes |
Testing
Tests verify APG compliance across keyboard interaction, ARIA attributes, and accessibility requirements. The Disclosure component uses E2E tests across all four frameworks.
Testing Strategy
E2E Tests (Playwright)
Verify component behavior in a real browser environment across all four frameworks. These tests cover interactions that require full browser context.
- Keyboard interactions (Space, Enter)
- aria-expanded state toggling
- aria-controls panel association
- Panel visibility synchronization
- Disabled state behavior
- Focus management and Tab navigation
- Cross-framework consistency
Test Categories
High Priority: APG ARIA Structure
| Test | Description |
|---|---|
button element | Trigger is a semantic <button> element |
aria-expanded | Button has aria-expanded attribute |
aria-controls | Button references panel ID via aria-controls |
accessible name | Button has an accessible name from content or aria-label |
High Priority: APG Keyboard Interaction
| Test | Description |
|---|---|
Space key toggles | Pressing Space toggles the disclosure state |
Enter key toggles | Pressing Enter toggles the disclosure state |
Tab navigation | Tab key moves focus to disclosure button |
Disabled Tab skip | Disabled disclosures are skipped in Tab order |
High Priority: State Synchronization
| Test | Description |
|---|---|
aria-expanded toggle | Click changes aria-expanded value |
panel visibility | Panel visibility matches aria-expanded state |
collapsed hidden | Panel content is hidden when collapsed |
expanded visible | Panel content is visible when expanded |
High Priority: Disabled State
| Test | Description |
|---|---|
disabled attribute | Disabled disclosure has disabled attribute |
click blocked | Disabled disclosure doesn't toggle on click |
keyboard blocked | Disabled disclosure doesn't toggle on keyboard |
Medium Priority: Accessibility
| Test | Description |
|---|---|
axe (collapsed) | No WCAG 2.1 AA violations in collapsed state |
axe (expanded) | No WCAG 2.1 AA violations in expanded state |
Testing Tools
- Vitest (opens in new tab) - Test runner for unit tests
- Testing Library (opens in new tab) - Framework-specific testing utilities (React, Vue, Svelte)
- Playwright (opens in new tab) - Browser automation for E2E tests
- axe-core/playwright (opens in new tab) - Automated accessibility testing in E2E
See testing-strategy.md (opens in new tab) for full documentation.
Resources
- WAI-ARIA APG: Disclosure Pattern (opens in new tab)
- MDN: <details> element (opens in new tab)
- AI Implementation Guide (llm.md) (opens in new tab) - ARIA specs, keyboard support, test checklist