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
<script lang="ts">
import type { Snippet } from 'svelte';
/**
* APG Disclosure Pattern - Svelte Implementation
*
* A button that controls the visibility of a section of content.
*
* @see https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/
*/
/**
* Props for the Disclosure component
*/
interface DisclosureProps {
/** Unique identifier (recommended for SSR-safe aria-controls) */
id?: string;
/** Content displayed in the disclosure trigger button */
trigger: string;
/** Content displayed in the collapsible panel */
children?: Snippet;
/** When true, the panel is expanded on initial render */
defaultExpanded?: boolean;
/** When true, the disclosure cannot be expanded/collapsed */
disabled?: boolean;
/** Additional CSS class */
className?: string;
/** Callback fired when the expanded state changes */
onExpandedChange?: (expanded: boolean) => void;
}
let {
id,
trigger,
children,
defaultExpanded = false,
disabled = false,
className = '',
onExpandedChange = () => {},
}: DisclosureProps = $props();
// Generate fallback ID if not provided (client-side only, may cause SSR mismatch)
const instanceId = id ?? crypto.randomUUID();
let expanded = $state(defaultExpanded);
const panelId = `${instanceId}-panel`;
function handleToggle() {
if (disabled) return;
expanded = !expanded;
onExpandedChange(expanded);
}
</script>
<div class="apg-disclosure {className}">
<button
type="button"
aria-expanded={expanded}
aria-controls={panelId}
{disabled}
class="apg-disclosure-trigger"
onclick={handleToggle}
>
<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" />
</svg>
</span>
<span class="apg-disclosure-trigger-content">{trigger}</span>
</button>
<div
id={panelId}
class="apg-disclosure-panel"
aria-hidden={!expanded}
inert={!expanded ? true : undefined}
>
<div class="apg-disclosure-panel-content">
{@render children?.()}
</div>
</div>
</div> Usage
<script>
import Disclosure from './Disclosure.svelte';
</script>
<Disclosure
id="my-disclosure"
trigger="Show details"
defaultExpanded={false}
onExpandedChange={(expanded) => console.log('Expanded:', expanded)}
>
<p>Hidden content that can be revealed</p>
</Disclosure> API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | auto-generated | Unique identifier for aria-controls (recommended for SSR) |
trigger | string | required | Content displayed in the trigger button |
children | Snippet | - | Content displayed in the panel |
defaultExpanded | boolean | false | Initially expanded state |
disabled | boolean | false | Disable the disclosure |
className | string | "" | Additional CSS class |
onExpandedChange | (expanded: boolean) => void | - | Callback 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