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.astro
---
/**
 * 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

Example
---
import Disclosure from './Disclosure.astro';
---

<Disclosure trigger="Show details" defaultExpanded={false}>
  <p>Hidden content that can be revealed</p>
</Disclosure>

API

PropTypeDefaultDescription
triggerstringrequiredContent displayed in the trigger button
slot (default)any-Content displayed in the panel
defaultExpandedbooleanfalseInitially expanded state
disabledbooleanfalseDisable the disclosure
classstring""Additional CSS class

Custom Events

EventDetailDescription
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 (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