Disclosure
A button that shows and hides a section of content. The simplest expand/collapse, and the building block for accordions.
Basic usage
Install the package:
pnpm add @design-schema/react @design-schema/tokens Import the stylesheet once for the app, then the component:
import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { Disclosure } from '@design-schema/react'; Examples
import { Disclosure, Text } from '@design-schema/react';
const panel = (
<Text>
You keep access until the end of the current billing period. Your data is kept for 30 days after that, then
deleted.
</Text>
);
<Disclosure summary="What happens if I cancel?">{panel}</Disclosure>Props
8 rows| Prop | Type | Default | Description |
|---|---|---|---|
| summary required | string | — | The trigger's label. Also the trigger's accessible name. Says what will be revealed. |
| children required | content | — | The content of the panel. Rendered only while open (not merely hidden), so heavy content is not laid out until asked for. On React Native the component (not the consumer) wraps string or number children in the package `Text`, which gives them `panelColor` through Text's default tone; other children keep their own colour, since native has no style inheritance. |
| open | boolean | — | Controlled open state. Omit for an uncontrolled disclosure. |
| defaultOpen | boolean | false | Initial state for an uncontrolled disclosure. |
| disabled | boolean | false | The trigger cannot be activated. Stays focusable and is announced as disabled. Only the trigger is dimmed (`disabledOpacity`); the panel keeps its current state and full opacity. |
| keepMounted | boolean | false | Keep the panel in the tree while closed (hidden, not unmounted). Required when the panel contains form fields, so the Form still collects them while the disclosure is closed. |
| fullWidth | boolean | false | The trigger spans the width of its row, so the whole row is the hit area (and the hover fill runs edge to edge) rather than ending at the summary text. The icon and summary stay at the start; the rest of the row is empty trigger. Web and Lit make the trigger a full-width block-level flex box (Lit: the host and the inner button both); native stretches the Pressable across its container. Accordion sets it on every section it renders. |
| headingLevel | 2 | 3 | 4 | 5 | 6 | — | When set, the trigger is wrapped in a heading of this level so the disclosure appears in the document outline — use for FAQ and accordion sections. |
Events
1 row| Event | Type | Description |
|---|---|---|
| onToggle | onToggle / toggle | Fired after the state changes, with the new boolean `open` and a reason: `pointer`, `keyboard`, or `controlled` (Accordion relies on it). Web and Lit read the activation method from the native click (`event.detail === 0` means keyboard); Pressable reports nothing of the kind, so native always says `pointer` for a trigger press (keyboard activation cannot be told apart) and Accordion derives its own reasons there. The payload is `{ open, reason }` on every platform (Lit: `event.detail`). A controlled `open` change the component did not cause reports `controlled`; when the consumer sets `open` to the value a user toggle already reported, that echo does not fire again (a pending request that is never echoed is cleared at the next `open` change); nothing fires on mount. Only the latest user request is remembered, so a second press before the consumer echoes replaces the first. The next `open` change clears the request, and if its value equals the remembered request it counts as the echo and does not report `controlled`, even when the consumer ignored the press and reached that value for its own reasons. In controlled mode `after-change` means after the component's request, not after a committed state: the event fires from the press handler with the requested value, because the state changes only when the consumer passes `open` back. Uncontrolled, it fires after the new state is committed. |
Accessibility
Decisions this component leaves to you
Supply an accessible name. This component has no text of its own to fall back on, so without one a screen reader announces its role and nothing else.
Implicit ARIA role: button.
Anatomy
What generation guarantees
Accessible name — Every instance carries a programmatic name. Where the component has no text of its own to use, the name is a prop the consumer supplies.
Expanded state — Open and closed are exposed programmatically (`aria-expanded`), not only visually.
Visible focus — Keyboard focus is always drawn, from the theme’s focus tokens, and is never removed.
Keyboard operable — Everything the pointer can do here, the keyboard can do — see the keyboard section.
Target size — Interactive targets are at least 24×24 CSS pixels (WCAG 2.2 Target Size, minimum).
Contrast — Text and meaningful non-text contrast meets WCAG AA; the pairs are listed below.
Reduced motion — Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.
Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.