APG Patterns
日本語
日本語

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.

Open demo only →

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 CaseNative HTMLCustom Implementation
Simple toggle contentRecommendedNot needed
JavaScript disabled supportWorks nativelyRequires fallback
Smooth animationsLimited supportFull control
External state controlLimitedFull control
Custom stylingBrowser-dependentFull 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

Disclosure.svelte
<script lang="ts">
  import type { Snippet } from 'svelte';
  import { untrack } 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 = untrack(() => id) ?? crypto.randomUUID();

  let expanded = $state(untrack(() => 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

Example
<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

PropTypeDefaultDescription
idstringauto-generatedUnique identifier for aria-controls (recommended for SSR)
triggerstringrequiredContent displayed in the trigger button
childrenSnippet-Content displayed in the panel
defaultExpandedbooleanfalseInitially expanded state
disabledbooleanfalseDisable the disclosure
classNamestring""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 (E2E)

TestDescription
button elementTrigger is a semantic <code><button></code> element
aria-expandedButton has aria-expanded attribute
aria-controlsButton references panel ID via aria-controls
accessible nameButton has an accessible name from content or aria-label

High Priority: APG Keyboard Interaction (E2E)

TestDescription
Space key togglesPressing Space toggles the disclosure state
Enter key togglesPressing Enter toggles the disclosure state
Tab navigationTab key moves focus to disclosure button
Disabled Tab skipDisabled disclosures are skipped in Tab order

High Priority: State Synchronization (E2E)

TestDescription
aria-expanded toggleClick changes aria-expanded value
panel visibilityPanel visibility matches aria-expanded state
collapsed hiddenPanel content is hidden when collapsed
expanded visiblePanel content is visible when expanded

High Priority: Disabled State (E2E)

TestDescription
disabled attributeDisabled disclosure has disabled attribute
click blockedDisabled disclosure doesn't toggle on click
keyboard blockedDisabled disclosure doesn't toggle on keyboard

Medium Priority: Accessibility (E2E)

TestDescription
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

See the Testing Strategy guide for details.

Resources