Disclosure
A button that controls the visibility of a section of content.
Demo
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 Properties
aria-controls
Associates the button with the panel it controls
- Values
- ID reference to panel
- Required
- Yes
aria-hidden
Hides panel from assistive technology when collapsed
- Values
- true | false
- Required
- No
WAI-ARIA States
aria-expanded
- Target Element
- button element
- Values
- true | false
- Required
- Yes
- Change Trigger
- Click, Enter, Space
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.
References
Source Code
<template>
<div :class="['apg-disclosure', className]">
<button
type="button"
:aria-expanded="expanded"
:aria-controls="panelId"
:disabled="disabled"
class="apg-disclosure-trigger"
@click="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">
<slot name="trigger">{{ trigger }}</slot>
</span>
</button>
<div
:id="panelId"
class="apg-disclosure-panel"
:aria-hidden="!expanded"
:inert="!expanded || undefined"
>
<div class="apg-disclosure-panel-content">
<slot />
</div>
</div>
</div>
</template>
<script setup lang="ts">
/**
* APG Disclosure Pattern - Vue Implementation
*
* A button that controls the visibility of a section of content.
*
* @see https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/
*/
import { ref, useId } from 'vue';
/**
* Props for the Disclosure component
*
* @example
* ```vue
* <Disclosure trigger="Show details">
* <p>Hidden content that can be revealed</p>
* </Disclosure>
* ```
*/
export interface DisclosureProps {
/** Content displayed in the disclosure trigger button (can also use #trigger slot) */
trigger?: string;
/** When true, the panel is expanded on initial render @default false */
defaultExpanded?: boolean;
/** When true, the disclosure cannot be expanded/collapsed @default false */
disabled?: boolean;
/** Additional CSS class @default "" */
className?: string;
}
const props = withDefaults(defineProps<DisclosureProps>(), {
trigger: '',
defaultExpanded: false,
disabled: false,
className: '',
});
const emit = defineEmits<{
expandedChange: [expanded: boolean];
}>();
const instanceId = useId();
const panelId = `${instanceId}-panel`;
const expanded = ref(props.defaultExpanded);
const { className } = props;
const handleToggle = () => {
if (props.disabled) return;
expanded.value = !expanded.value;
emit('expandedChange', expanded.value);
};
</script> Usage
<script setup>
import Disclosure from './Disclosure.vue';
</script>
<template>
<Disclosure
trigger="Show details"
:default-expanded="false"
@expanded-change="(expanded) => console.log('Expanded:', expanded)"
>
<p>Hidden content that can be revealed</p>
</Disclosure>
</template> API
| Prop | Type | Default | Description |
|---|---|---|---|
trigger | string | "" | Content displayed in the trigger button (or use #trigger slot) |
defaultExpanded | boolean | false | Initially expanded state |
disabled | boolean | false | Disable the disclosure |
className | string | "" | Additional CSS class |
Slots
| Slot | Default | Description |
|---|---|---|
default | - | Content displayed in the panel |
#trigger | - | Custom trigger content (alternative to trigger prop) |
Custom Events
| Event | Detail | Description |
|---|---|---|
expandedChange | boolean | Emitted 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 (E2E)
| Test | Description |
|---|---|
button element | Trigger is a semantic <code><button></code> 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 (E2E)
| 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 (E2E)
| 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 (E2E)
| 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 (E2E)
| Test | Description |
|---|---|
axe (collapsed) | No WCAG 2.1 AA violations in collapsed state |
axe (expanded) | No WCAG 2.1 AA violations in expanded state |
Running Tests
# Run E2E tests for Disclosure (all frameworks)
npm run test:e2e:pattern --pattern=disclosure 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