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.
Figma↔Code mismatches
Where designer and developer worlds typically misalign on this component.
- 01 Figma
Accordion item header drawn as plain text with a chevron icon
CodeA real `<button>` inside a `<h2>`/`<h3>`/`<h4>` element with `aria-expanded`
ConsequenceDesigners 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).
CorrectDocument 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.
- 02 Figma
Single-open and multi-open drawn as the same component
CodeA `multi` boolean property toggling whether multiple panels can be open simultaneously
ConsequenceDesigners 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.
CorrectTreat `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.
- 03 Figma
Animated chevron rotation drawn but no panel-height transition
CodeBoth the chevron rotation AND the 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.
CorrectDocument 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`.
- 04 Figma
Each item header drawn at a different heading level for "visual variety"
CodeAll item headers at the same heading level (consistent with document outline)
ConsequenceDesigners 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.
CorrectAll 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.
Variants, properties, states
Variants
Structurally different versions of the component.
borderedcontainedflush Properties
The same component, parameterised.
| Property | Type |
|---|---|
multi | boolean |
collapsible | boolean |
density | comfortable | compact |
States
Browser/user-driven (interactive) vs. app-driven (data).
| Kind | States |
|---|---|
interactive | hoverfocus-visibleactivedisabled |
data | closedexpandingexpandedcollapsing |
Figma ↔ Code property map
| Figma | Type | Code | Notes |
|---|---|---|---|
Variant | Variant | variant | Maps bordered / contained / flush. |
Multi | Boolean | multi | Toggles single-vs-multi-open behaviour. Default false (single-open) for FAQ patterns; true for settings groups. |
Collapsible | Boolean | collapsible | In 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. |
Density | Variant | density | comfortable / compact. At and below `breakpoint.sm` density compact is the canonical default. |
Item Count | Variant | items.length | Figma exposes 2/3/4/5/6+ item counts as a Variant for preview-time layout review. Code accepts an array of item definitions. |
Has Icon | Boolean | hasIcon | Toggles the trigger-icon (chevron / plus-minus) visibility per item. |
Default Open Items | Variant | defaultOpen | Figma 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). |
State transitions
| From | To | Trigger |
|---|---|---|
closed | expanding | User activates the trigger (Enter / Space / click) on a closed item. `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 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. |
collapsing | closed | The 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`. |
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 |
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 |
Cross-framework expression
| Framework | Structure mechanism | Variant 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 |
Events
expandedChangeitemActivate
Performance thresholds
lazyMountThresholdpanel-payload-size≥50kbAccordion 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-items≥12itemsAbove ~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.
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.
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. |
Accessibility acceptance
Keyboard walk
| Keys | Expected |
|---|---|
Tab | Focus 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+Tab | Reverse 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
| Trigger | Expected |
|---|---|
| Focus enters trigger, item closed | SR 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 expands | SR 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 item | On 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-rolearia-required-attraria-valid-attr-valueheading-ordercolor-contrastregion
Common mistakes
#accordion-no-heading-wrap
Trigger button not wrapped in a heading element
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.
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
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.
`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
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.
`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-anchor-link-no-expand
Page anchor link to accordion content does not expand the panel
A link elsewhere in the document points at content inside a closed accordion panel (e.g. `href="#shipping-policy"`). The user follows the link; the page scrolls to the panel but the panel is collapsed and the content is not visible. Anchor navigation without auto-expand is a dead-end.
On hashchange, expand the accordion item containing the target. Common pattern: bind a `hashchange` listener on the accordion that finds the matching item and toggles it open before scrolling. Mature primitives (Radix Accordion plus a `toItem` API; React Aria) expose hooks for this.
#accordion-multi-vs-single-confusion
Single-open mode silently switches to multi when user expects mutual exclusion
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.
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.