Bridge view

Accordion

A vertically-stacked set of disclosure controls, each pairing a header button with a panel of content that expands or collapses. Distinct from Tabs (always shows one panel, replaces on selection) by allowing zero, one, or multiple panels open at once and by preserving all panel labels on screen. Used for FAQs, settings groupings, progressive disclosure of dense reference content, and any list of named regions where content is browsable but not all needed simultaneously.

When to use

Use

For a list of named regions where content is browsable but not all needed simultaneously — FAQs, settings groupings, progressive disclosure of dense reference content. The user can scan all headers, expand the items they need, and collapse the ones they don't. Accordion preserves all panel labels on screen.

Avoid

For mutually-exclusive parallel views of the same subject (settings tabs, alternative presentations of the same data) — that is `Tabs`. For a single collapsible region not part of a list — that is `Disclosure`. For navigation between independent pages — that is `SidebarNav`. For sequential flows that must complete in order — that is `Stepper`.

Versus related

  • tabs

    `Tabs` always shows one panel and replaces it on selection; `Accordion` allows zero, one, or multiple panels open at once. Tabs are lateral (peer content chunks at the same level); Accordion is hierarchical (each header is a heading in the document outline). Vertical Tabs and single-mode Accordion can look similar — the distinguisher is whether all labels stay visible (Accordion) or only the selected label is emphasised (Tabs).

  • disclosure

    `Disclosure` is a single collapsible region with one toggle; `Accordion` is a list of disclosures grouped as a unit with shared keyboard navigation (ArrowDown / Up between items). A standalone collapsible region is Disclosure, not Accordion-of-one-item.

  • sidebar-nav

    `SidebarNav` navigates between independent pages with their own URLs; `Accordion` discloses inline content within a single page. A nav-style accordion (clicking an item navigates) is a SidebarNav with collapsible sections, not an Accordion.

Highlight
Fig 1.1 · Accordion · Bridge view
Both

Figma↔Code mismatches

Where designer and developer worlds typically misalign on this component.

  1. 01
    Figma

    Accordion item header drawn as plain text with a chevron icon

    Code

    A real `<button>` inside a `<h2>`/`<h3>`/`<h4>` element with `aria-expanded`

    Consequence

    Designers may treat the header as a styled label with a chevron affordance. Implementations following the Figma file ship a `<div>` with click handler — the click works for mouse users but the entire keyboard, SR, and APG contract is broken (no focus, no Enter/Space activation, no announced state, no heading semantics).

    Correct

    Document the canonical structure as heading-wraps-button. The Figma component must encode "header is heading; trigger is button" — even if the visual treatment puts no button-styling on the trigger, the underlying element is a real `<button>` inside a real heading.

  2. 02
    Figma

    Single-open and multi-open drawn as the same component

    Code

    A `multi` boolean property toggling whether multiple panels can be open simultaneously

    Consequence

    Designers do not differentiate the two modes in the design file; developers ship one or the other depending on what the Figma mock implies. The mode is canonical-meaningful — single-open accordions imply mutually-exclusive disclosure (FAQ patterns); multi-open imply independent collapsible regions (settings groups). Mixing them confuses users.

    Correct

    Treat `multi` as a first-class property on the canonical reference. Designers and developers both consult the canon to confirm which mode they're using; the Figma component carries `multi` as a Boolean property.

  3. 03
    Figma

    Animated chevron rotation drawn but no panel-height transition

    Code

    Both the chevron rotation AND the 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

    Document both animations as canonical: chevron rotation + panel height-transition share the same duration token (per ADR-007 motion). Implementations using `[hidden]` toggle without animation are valid for `prefers-reduced-motion: reduce`.

  4. 04
    Figma

    Each item header drawn at a different heading level for "visual variety"

    Code

    All item headers at the same heading level (consistent with document outline)

    Consequence

    Designers may use h2 for important items, h3 for secondary, h4 for tertiary in the same accordion to create visual hierarchy. Implementations following the design break SR heading navigation (the user expects a flat list of siblings; gets a hierarchy). Heading levels in an accordion communicate document structure, not item importance.

    Correct

    All accordion item headers MUST be the same heading level. Choose the level relative to the document outline (h2 if the accordion is a top-level region; h3 if nested under a top-level h2). Communicate item importance through content order, not heading hierarchy.

Both

Variants, properties, states

Variants

Structurally different versions of the component.

borderedcontainedflush

Properties

The same component, parameterised.

PropertyType
multiboolean
collapsibleboolean
densitycomfortable | compact

States

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

KindStates
interactive
hoverfocus-visibleactivedisabled
data
closedexpandingexpandedcollapsing
Both

Figma ↔ Code property map

FigmaTypeCodeNotes
VariantVariantvariantMaps bordered / contained / flush.
MultiBooleanmultiToggles single-vs-multi-open behaviour. Default false (single-open) for FAQ patterns; true for settings groups.
CollapsibleBooleancollapsibleIn single-open mode, allows the currently-open item to collapse (state with no items expanded). Multi-mode always allows collapse; this property is only meaningful for single-mode.
DensityVariantdensitycomfortable / compact. At and below `breakpoint.sm` density compact is the canonical default.
Item CountVariantitems.lengthFigma exposes 2/3/4/5/6+ item counts as a Variant for preview-time layout review. Code accepts an array of item definitions.
Has IconBooleanhasIconToggles the trigger-icon (chevron / plus-minus) visibility per item.
Default Open ItemsVariantdefaultOpenFigma exposes "first item open", "all items open", "no items open" as Variant for preview; in code it's a configuration prop (single-string id for single-mode, array of ids for multi-mode).
Both

State transitions

FromToTrigger
closedexpandingUser activates the trigger (Enter / Space / click) on a closed item. `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 on an expanded item (when `collapsible: true` for single-open variant, or always for multi-open variant); or another item is activated in single-open non-collapsible mode and this item collapses to make way.
collapsingclosedThe collapse animation completes (or immediately under reduced motion). The panel is removed from the accessibility tree and from the keyboard tab order via the `hidden` attribute or `display: none`.
Designer

Figma anatomy

Slot Figma type Hint
root frame Auto-layout vertical frame; gap and border treatment from variant
item instance Accordion item component instance; expanded state via component property
header text Heading text style; same level for every item in the accordion
trigger instance Button component instance filling the header; bound to expanded state
icon instance Icon component instance; rotation or swap bound to expanded state
panel frame Auto-layout vertical frame; visibility bound to expanded state of the parent item
Dev

Code anatomy

Slot Code slot Semantic
root root presentational-list
item item container
header header heading
trigger trigger button
icon icon presentational
panel panel region
Dev

Cross-framework expression

FrameworkStructure mechanismVariant mechanism
Web Components A `<ui-accordion>` host with `<ui-accordion-item>` children; each item composes a `<button>` inside a heading element plus a `<div>` panel; light DOM for content projection attributes (`variant="bordered"`, `multi`, `collapsible`, `density="comfortable"`); `data-state="closed|expanded|expanding|collapsing"` on each item
React Compound components (Radix `Accordion.Root` / `Accordion.Item` / `Accordion.Header` / `Accordion.Trigger` / `Accordion.Content`); React Aria `useDisclosure` plus a custom collection composer props on `Accordion.Root` (variant / type / collapsible); `data-state` exposed for styling; `Accordion.Trigger` automatically wires the heading semantics via `asChild`
Angular (signals) Angular CDK A11y `cdk-accordion` plus signal-based items; content projection for header content and panel content input<'bordered' | 'contained' | 'flush'>(); `[multi]`, `[collapsible]` host bindings; per-item `[expanded]` two-way binding
Vue Headless UI does not ship Accordion as of 2026-04; third-party (Radix Vue, vue-collapsible); custom composable `useAccordion` plus per-item `useDisclosure` defineProps with literal-union types; `:multi`, `:collapsible` props
Both

Events

  1. expandedChange
    Payload
    `{ itemId: string, expanded: boolean }`. Fires when an individual item's expanded state changes. For multi-open accordions the event fires per item; for single-open accordions activating one item may fire two events (the previously-open item closing, the new item expanding).
    Web Components
    `expandedChange` CustomEvent on the host with `event.detail = { itemId, expanded }`.
    React
    `onValueChange(value: string | string[])` on Radix `Accordion.Root` — for `type="single"` the value is the currently-open item id (or empty string when none); for `type="multiple"` the value is an array of open ids. Consumers may derive per-item events from the before/after diff.
    Angular Signals
    `output<{ itemId: string, expanded: boolean }>('expandedChange')` on the accordion or per-item.
    Vue
    `@update:modelValue` for `v-model` on the accordion's open-items list.
  2. itemActivate
    Payload
    `{ itemId: string }`. Fires when the user activates an item's trigger — distinct from `expandedChange` because activating an already-expanded single-open item with `collapsible: false` does NOT emit `expandedChange` (state unchanged) but DOES emit `itemActivate`. Lets consumers track user intent independently of state changes.
    Web Components
    `itemActivate` CustomEvent with `event.detail = { itemId }`.
    React
    Uncommon as a discrete event; consumers wire `onClick` on `Accordion.Trigger` to capture activation.
    Angular Signals
    `output<string>('itemActivate')`.
    Vue
    `@activate` event with payload `{ itemId }`.
Both

Performance thresholds

  • lazyMountThresholdpanel-payload-size50kb

    Accordion panels above ~50kb of rendered DOM payload should lazy-mount on first expand. Below the threshold, eager-render all panels (allows `[hidden]` toggle without mount cost). Above the threshold, the per-mount cost dominates and the lazy-mount pattern (per the `tabs-lazy-panel-no-aria-busy` mistake on Tabs which also applies to Accordion) earns its complexity. Threshold is lower than Tabs (100kb) because accordions tend to render more total items in the open state simultaneously (multi-open variant).

  • itemCounttotal-items12items

    Above ~12 accordion items, the disclosure pattern stops working — the user cannot scan the list of headers without scrolling, and the cognitive load of remembering which items have been expanded exceeds disclosure's benefit. Above 12 items, redesign as a paginated list, a filterable list, or split the content into nested accordions by category.

Both

Internationalisation

RTL · mirroring

Vertical orientation is direction-neutral (top-to-bottom stacking unchanged). Trigger inline-content (heading-text · chevron) reverses logical order — the chevron moves from inline-end (visual right in LTR) to inline-end (visual left in RTL) via logical positioning. ArrowDown / ArrowUp keyboard navigation is unchanged. Chevron rotation is direction-neutral (down/up arrows are symmetric); plus-minus glyph is also direction-neutral.

Text expansion

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

Both

Accessibility

Slot Accessibility hint
root The root has no required ARIA role. APG explicitly forbids `role="region"` on the root because the regions live on the panels inside (each panel is its own region). Adding a wrapping role creates redundant landmark navigation.
item Item is a structural grouping; no ARIA role. The header and panel inside carry the canonical APG semantics (heading + button + region).
header Header is a heading element — `<h2>` to `<h4>` depending on document outline. The button inside is what carries `aria-expanded`; the heading itself does not. Using non-heading wrappers (a `<div>` with `role="heading"`) breaks APG conformance.
trigger Real `<button>` — never a `<div>` with click handler. Carries `aria-expanded` reflecting state, `aria-controls` referencing the panel id, and the accessible name from the header text. Disabled headers use `aria-disabled` to stay focusable; sequential focus skips disabled items only via roving tabindex (rare in accordion canon).
icon Decorative — `aria-hidden="true"`. Expansion state is communicated through `aria-expanded` on the trigger and through the panel's visible height; the icon is visual reinforcement only. Icon-only state cues without `aria-expanded` violate APG.
panel Apply `role="region"` with `aria-labelledby` referencing the trigger's id. The panel is hidden via the `hidden` attribute or `display: none` when collapsed (not just zero-height) so SR users do not encounter ghost content. For `motion: collapse` animations, swap `hidden` for a `aria-hidden="true"` plus `pointer-events: none` while the height transition runs, then apply `hidden` at the end.
Both

Accessibility acceptance

Keyboard walk

KeysExpected
TabFocus enters the first item's trigger. Subsequent Tab moves to the next focusable INSIDE the open panel, then out of the panel to the next item's trigger, then through the rest of the document. Closed panels skip from focus order entirely.
Shift+TabReverse direction — moves backwards through panel contents and trigger buttons.
Enter or Space (focus on trigger)Toggles the item's expanded state. For single-open non-collapsible mode, activating the currently-expanded item is a no-op (panel stays open); collapsible mode allows close.
ArrowDown / ArrowUp (focus on trigger)Moves focus to the next / previous trigger in the accordion (per APG optional-but-recommended). Wraps from last to first and vice versa. NOT activation — focus only.
Home / End (focus on trigger)Moves focus to the first / last trigger in the accordion (per APG optional).

Screen-reader announcements

TriggerExpected
Focus enters trigger, item closedSR announces the heading text as part of heading navigation, then the trigger as "<heading text>, button, collapsed" (or "expanded" if open). `aria-expanded` provides the state portion.
Trigger activated, item expandsSR re-announces the trigger with its new state: "<heading text>, 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 itemOn hashchange + auto-expand, focus should move to the target inside the panel (or to the panel's heading 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
  • heading-order
  • color-contrast
  • region
Both

Common mistakes

#accordion-no-heading-wrap

Trigger button not wrapped in a heading element

Problem

The trigger is a `<button>` directly inside a `<div>` or a list item, not wrapped in a heading. SR users navigating by heading skip the accordion entirely; the structure lacks the canonical APG semantics.

Fix

Wrap each trigger in a heading element (`<h2>`, `<h3>`, `<h4>`) of consistent level across all items. The heading is what SR users use to navigate to and within the accordion; the button inside the heading is what carries the toggle behaviour.

#accordion-no-aria-expanded

Trigger missing `aria-expanded` toggle

Problem

The button has no `aria-expanded` attribute. Visually the panel reveals; SR users hear "button" with no state cue, and the icon alone is not discoverable 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.

#accordion-icon-only-state-cue

Expansion state communicated only by chevron rotation

Problem

The chevron rotates on expand, but `aria-expanded` is missing or stuck at one value. 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 for expansion state; the icon visualises it. The icon is `aria-hidden="true"`; the announced state comes from `aria-expanded`. Style icon rotation from `[aria-expanded="true"]`.

#accordion-multi-vs-single-confusion

Single-open mode silently switches to multi when user expects mutual exclusion

Problem

The accordion's `multi` mode is set by config but not communicated to users. Users expanding one item expect the previously-open item to collapse (single-open assumption); it does not, and the layout grows unexpectedly.

Fix

Choose `multi` mode based on content: single-open for mutually-exclusive content (FAQ patterns), multi-open for independent regions (settings groups). The mode should be visually obvious — single-open's collapse-others behaviour gives a recognisable "only one open" feeling. Avoid switching modes mid-flow; the choice is canonical per use case.