Dev view
Disclosure
A single collapsible region with one toggle — a button that expands or collapses an associated content panel. The atomic unit of disclosure: Accordion is a list of disclosures grouped with shared keyboard navigation, but a standalone Disclosure has no peers and no group semantics. Used for "show more" patterns in prose, expandable details rows, optional advanced settings, inline help expansion, and any single named region whose content is disclosable on demand.
When to use
Use
For a single collapsible region — "show more" patterns in prose, expandable details rows, optional advanced settings, inline help expansion, expandable filter sections. The user toggles the disclosure to reveal or hide content that is not always needed.
Avoid
For a list of disclosures grouped with shared keyboard navigation — that is `Accordion`. For mutually-exclusive parallel views — that is `Tabs`. For navigation between independent pages — that is `SidebarNav`. For blocking content reveals — that is `Modal` or `Drawer`.
Versus related
- accordion
`Accordion` is a list of disclosures grouped as a unit with shared keyboard navigation (ArrowDown / Up between items, Home / End to first / last). `Disclosure` is a single collapsible region with no peers and no group semantics. Accordion-of-one-item is anti-pattern; use Disclosure when there's only one region.
- tabs
`Tabs` always shows one panel at a time, replaces on selection, and has visible peer labels for unselected panels. `Disclosure` shows zero or one panel and has no peers. Tabs are lateral; Disclosure is hierarchical (the trigger is a heading or sentence-fragment in the document outline).
- modal
`Modal` blocks the page and demands explicit response; `Disclosure` is non-blocking and reveals inline. Modal severs spatial relationship to the underlying content; Disclosure preserves it.
Code anatomy
| Slot | Code slot | Semantic |
|---|---|---|
trigger | trigger | button |
icon | icon | presentational |
panel | panel | region |
Variants, properties, states
Variants
Structurally different versions of the component.
inlinestandalone Properties
The same component, parameterised.
| Property | Type |
|---|---|
defaultExpanded | boolean |
density | comfortable | compact |
hasIcon | boolean |
States
Browser/user-driven (interactive) vs. app-driven (data).
| Kind | States |
|---|---|
interactive | hoverfocus-visibleactivedisabled |
data | closedexpandingexpandedcollapsing |
State transitions
| From | To | Trigger |
|---|---|---|
closed | expanding | User activates the trigger (Enter / Space / click). `aria-expanded` flips to true; the panel begins its enter animation. |
expanding | expanded | The expand animation completes (or, under prefers-reduced-motion reduce, immediately). The panel is fully visible and reachable by keyboard via Tab. |
expanded | collapsing | User activates the trigger again (toggle is symmetric on Disclosure — unlike single-mode Accordion which may have non-collapsible behaviour). |
collapsing | closed | The collapse animation completes (or immediately under reduced motion). The panel is removed from the accessibility tree and from keyboard tab order via `hidden` or `display: none`. |
Cross-framework expression
| Framework | Structure mechanism | Variant mechanism |
|---|---|---|
| Web Components | A `<ui-disclosure>` host with named slots for `trigger`, `panel`, optional `icon`; trigger is a real `<button>` with `aria-expanded` reflected from the host's open state | attributes (`variant="standalone"`, `default-expanded`, `density="comfortable"`); `data-state="closed|expanded|expanding|collapsing"` for CSS |
| React | Compound components (Radix uses `Collapsible.Root` / `Collapsible.Trigger` / `Collapsible.Content`; React Aria `useDisclosure` plus a custom collection composer; Headless UI ships `<Disclosure>` / `<DisclosureButton>` / `<DisclosurePanel>`) | props with class-variance-authority for variant; `defaultOpen` boolean; `data-state` exposed for styling |
| Angular (signals) | Angular CDK `cdk-accordion` plus `cdk-accordion-item` (single-item case) or a custom directive plus signal-based open state | input<'inline' | 'standalone'>(); `[defaultExpanded]` host binding; `[(open)]` two-way binding |
| Vue | Headless UI `<Disclosure>` / `<DisclosureButton>` / `<DisclosurePanel>`; or a custom composable `useDisclosure` plus per-item `useToggle` | defineProps with literal-union types; `:default-open` boolean |
Events
openChange
Performance thresholds
lazyMountThresholdpanel-payload-size≥100kbDisclosure panels above ~100kb of rendered DOM payload should lazy-mount on first expand. Below the threshold, eager-render and use `[hidden]` toggle (allows CSS-only show/hide and avoids per-expand mount latency). Above the threshold, the per-mount cost dominates and lazy-mount with `aria-busy` during async loading is the canonical pattern. Threshold matches Tabs's panel threshold; higher than Accordion's (50kb) because a single Disclosure does not compete with siblings for memory budget.
Accessibility
| Slot | Accessibility hint | |
|---|---|---|
trigger | Real `<button>` — never a `<div>` with click handler nor an `<a>` (anchors navigate; disclosures toggle in-page state). Carries `aria-expanded="true|false"` and `aria-controls` referencing the panel's id. Standalone disclosures may be wrapped in a heading element when the disclosure represents a content region; inline disclosures (within prose) are not heading-wrapped. | |
icon | Decorative — `aria-hidden="true"`. State is communicated through `aria-expanded` on the trigger; the icon is visual reinforcement only. | |
panel | Apply `aria-labelledby` referencing the trigger's id (or `aria-label` if the trigger has no useful text content for labelling). For standalone disclosures with regional content, `role="region"` is appropriate; for inline disclosures in prose, no role needed (the content retains its native semantics). Hide via `hidden` attribute or `display: none` when collapsed (not just zero-height) so SR users do not encounter ghost content. |
Accessibility acceptance
Keyboard walk
| Keys | Expected |
|---|---|
Tab | Focus enters the trigger. Subsequent Tab moves to the next focusable inside the open panel; Tab continues through the panel content then exits the disclosure into the rest of the page. Closed panels skip from focus order entirely. |
Shift+Tab | Reverse direction — moves backwards through panel contents and trigger. |
Enter or Space (focus on trigger) | Toggles the disclosure's expanded state. Symmetric — unlike single-mode Accordion which may have non-collapsible behaviour, Disclosure's toggle always works in both directions. |
Screen-reader announcements
| Trigger | Expected |
|---|---|
| Focus enters trigger (closed) | SR announces the trigger's accessible name followed by "button, collapsed". `aria-expanded` provides the state portion. |
| Trigger activated, panel expands | SR re-announces the trigger as "<accessible name>, button, expanded". The panel content becomes part of the document; subsequent Tab into the panel announces its content normally. |
| Anchor-link auto-expands a closed disclosure | On hashchange + auto-expand, focus should move to the target inside the panel (or to the trigger if no specific target). The user is not surprised by invisible expansion. |
axe-core rules to assert
aria-allowed-rolearia-required-attraria-valid-attr-valuecolor-contrastregion
Common mistakes
#disclosure-no-aria-expanded
Trigger missing `aria-expanded` toggle
The button has no `aria-expanded` attribute. Visually content reveals; SR users hear "button" with no state cue. Icon rotation alone is invisible to non-sighted users.
`aria-expanded="true"` on the button when the panel is open, `false` when closed. Always pair with `aria-controls` referencing the panel's id. Style the icon from `[aria-expanded="true"]` rather than introducing a parallel `data-expanded` attribute.
#disclosure-as-link
Disclosure trigger implemented as `<a>`
The trigger is an anchor element (because it looks like a link, or because the developer copy-pasted from another "Show more" link). Middle-click opens an empty page; copy-link captures `#`; semantically wrong because anchors navigate, disclosures toggle.
Use `<button type="button">` for disclosure triggers. The visual treatment may match link styling; the underlying element is a button. The semantic distinction (link navigates, button performs in-page action) applies here as it does for Button vs Link generally.
#disclosure-no-aria-controls
`aria-controls` not wired to the panel
Trigger has `aria-expanded` but no `aria-controls`. SR users know the trigger is expanded/collapsed but cannot navigate to the disclosed content directly. The relationship between trigger and panel is implicit (visual proximity) rather than explicit.
`aria-controls` references the panel's id. Pair with the panel's `aria-labelledby` referencing the trigger's id for the bidirectional relationship. Mature primitives (Radix, React Aria) wire this automatically.
#disclosure-icon-only-state-cue
Expansion state shown only via icon rotation
The chevron rotates on expand, but `aria-expanded` is missing or stuck. Sighted users see the state; SR users do not. The icon is decorative; without `aria-expanded` it carries the entire state-meaning load.
`aria-expanded` is the source of truth. The icon visualises the state. Style icon rotation from `[aria-expanded="true"]`. The icon is `aria-hidden`.
#disclosure-anchor-link-no-expand
Page anchor link to disclosed content does not expand
A link elsewhere in the document points at content inside a closed disclosure (e.g. `href="#shipping-policy"`). The page scrolls to the panel but the panel is collapsed. Anchor navigation without auto-expand is a dead-end for the user.
On hashchange, expand the disclosure containing the target. Listener finds the disclosure containing the target id and toggles it open before scrolling. Same-pattern fix as `accordion-anchor-link-no-expand`; the canonical contract spans both Accordion and Disclosure.
Figma↔Code mismatches
- 01 Figma
A "Show more" link drawn for inline disclosure in body prose
CodeA `<button>` toggling visibility of inline content
ConsequenceDesigners may use link-styling for "Show more" affordances (looks like other inline links in prose). Implementations following the Figma file ship `<a>` with click handlers — semantically wrong (anchors navigate, disclosures toggle), breaks middle-click expectations, and SR users hear "link" with no toggle-state cue.
CorrectUse `<button>` with disclosure semantics (`aria-expanded`, `aria-controls`). Visual styling may converge with link styling (underlined, accent colour) but the underlying element is a button. Document the visual-vs-semantic distinction in mismatches.
- 02 Figma
Disclosure drawn with chevron rotation but no panel-height transition
CodeBoth chevron rotation AND panel height-transition animate together
ConsequenceDesigners animate the chevron in mocks but the panel appears instantly (Figma cannot easily mock smooth height-transitions). Developers shipping faithful-to-mock get jumpy panel transitions; SR users get no announcement while the visible content shifts.
CorrectBoth the chevron rotation and the panel height-transition share the same duration token. Implementations using `[hidden]` toggle without animation are valid for `prefers-reduced-motion: reduce` (the canonical reducedMotionFallback is `instant`).
- 03 Figma
Standalone disclosure drawn without heading semantics
CodeStandalone disclosure with the trigger wrapped in a heading element
ConsequenceDesigners draw a "section" with a click-to-expand affordance but treat it as plain UI rather than as a document structural element. Implementations skip the heading wrapper; SR users navigating by heading miss the collapsible region's existence.
CorrectFor standalone disclosures representing document content regions (an FAQ entry, a settings section, a CSV import step), wrap the trigger in a heading element of the appropriate level. For inline disclosures in prose ("Show more" inside a paragraph), the trigger is not heading- wrapped. Document the variant-driven structural choice.
- 04 Figma
Disclosure used for a single Accordion-of-one-item
CodeA standalone Disclosure component, not Accordion
ConsequenceDesigners compose an Accordion with a single item for visual consistency with multi-item accordions elsewhere on the page. Developers ship an Accordion with one item; the user sees a collapsible region but the surrounding ARIA structure is wrong (no list semantics for a single item, no shared keyboard navigation that Accordion provides for multiple items).
CorrectA single collapsible region is a Disclosure, not an Accordion-of-one. Use Disclosure when there's only one collapsible region; use Accordion when there are multiple disclosures grouped with shared keyboard navigation (ArrowDown / Up between triggers).