Bridge 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.

Highlight
Fig 1.1 · Disclosure · Bridge view
Both

Figma↔Code mismatches

Where designer and developer worlds typically misalign on this component.

  1. 01
    Figma

    A "Show more" link drawn for inline disclosure in body prose

    Code

    A `<button>` toggling visibility of inline content

    Consequence

    Designers 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.

    Correct

    Use `<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.

  2. 02
    Figma

    Disclosure drawn with chevron rotation but no panel-height transition

    Code

    Both chevron rotation AND panel height-transition animate together

    Consequence

    Designers 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.

    Correct

    Both 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`).

  3. 03
    Figma

    Standalone disclosure drawn without heading semantics

    Code

    Standalone disclosure with the trigger wrapped in a heading element

    Consequence

    Designers 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.

    Correct

    For 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.

  4. 04
    Figma

    Disclosure used for a single Accordion-of-one-item

    Code

    A standalone Disclosure component, not Accordion

    Consequence

    Designers 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).

    Correct

    A 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).

Both

Variants, properties, states

Variants

Structurally different versions of the component.

inlinestandalone

Properties

The same component, parameterised.

PropertyType
defaultExpandedboolean
densitycomfortable | compact
hasIconboolean

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
hoverfocus-visibleactivedisabled
data
closedexpandingexpandedcollapsing
Both

Figma ↔ Code property map

FigmaTypeCodeNotes
VariantVariantvariantMaps inline / standalone. Drives heading-wrapping (standalone wraps trigger in heading; inline does not).
Default ExpandedBooleandefaultExpandedInitial open state. False for "show more" patterns; true for content that should be visible by default but collapsible for users who want to reduce noise.
DensityVariantdensitycomfortable / compact. At and below `breakpoint.sm` density compact is the canonical default for inline variant.
Has IconBooleanhasIconToggles the chevron / plus-minus icon. Default true; rare to disable.
Trigger LabelTexttriggerLabelThe button's accessible name. "Show more" / "Read details" / heading text per use case.
Panel ContentInstance SwapchildrenSwap the panel content slot — prose, form, list, custom layout.
IconInstance SwapiconSwap the chevron glyph; defaults vary per design system.
Both

State transitions

FromToTrigger
closedexpandingUser activates the trigger (Enter / Space / click). `aria-expanded` flips to true; the panel begins its enter animation.
expandingexpandedThe expand animation completes (or, under prefers-reduced-motion reduce, immediately). The panel is fully visible and reachable by keyboard via Tab.
expandedcollapsingUser activates the trigger again (toggle is symmetric on Disclosure — unlike single-mode Accordion which may have non-collapsible behaviour).
collapsingclosedThe 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`.
Designer

Figma anatomy

Slot Figma type Hint
trigger instance Button instance; visual treatment varies by variant (inline link-style, standalone block)
icon instance Icon component instance; rotation or swap bound to expanded state
panel frame Auto-layout vertical frame; visibility bound to expanded state
Dev

Code anatomy

Slot Code slot Semantic
trigger trigger button
icon icon presentational
panel panel region
Dev

Cross-framework expression

FrameworkStructure mechanismVariant 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
Both

Events

  1. openChange
    Payload
    Boolean. `true` when the panel finishes expanding, `false` when it finishes collapsing. Mirrors `aria-expanded` on the trigger.
    Web Components
    `openChange` CustomEvent on the host with `event.detail = { open: boolean }`.
    React
    `onOpenChange(open: boolean)` controlled-pattern callback (Radix Collapsible, Headless UI Disclosure exposes `open` render-prop instead of event but consumers may wrap).
    Angular Signals
    `output<boolean>('openChange')`; pair with `[(open)]` two-way binding.
    Vue
    `@update:open` for `v-model:open`.
Both

Performance thresholds

  • lazyMountThresholdpanel-payload-size100kb

    Disclosure 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.

Both

Internationalisation

RTL · mirroring

Trigger inline-content order reverses logically — the icon moves from inline-end (visual right in LTR) to inline-end (visual left in RTL) via logical positioning. Panel content inherits document direction. Chevron rotation is direction-neutral (down/up arrows are symmetric); plus-minus glyphs are also direction-neutral. Standalone-variant heading-wrapping is direction-neutral.

Text expansion

Trigger label wraps to additional lines under heavy expansion (DE / RU / FI). Panel content follows its own text-flow. Density compact risks crowding long-text trigger labels; density comfortable is the safer default in long-text locales. Multi-line trigger labels behave correctly with the icon — chevron aligns with the first line by canonical convention.

Both

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.
Both

Accessibility acceptance

Keyboard walk

KeysExpected
TabFocus 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+TabReverse 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

TriggerExpected
Focus enters trigger (closed)SR announces the trigger's accessible name followed by "button, collapsed". `aria-expanded` provides the state portion.
Trigger activated, panel expandsSR 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 disclosureOn 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-role
  • aria-required-attr
  • aria-valid-attr-value
  • color-contrast
  • region
Both

Common mistakes

#disclosure-no-aria-expanded

Trigger missing `aria-expanded` toggle

Problem

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.

Fix

`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-no-aria-controls

`aria-controls` not wired to the panel

Problem

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.

Fix

`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

Problem

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.

Fix

`aria-expanded` is the source of truth. The icon visualises the state. Style icon rotation from `[aria-expanded="true"]`. The icon is `aria-hidden`.