Accordion
Examples
Contained (default)
Card chrome with border, radius, and shadow. Items are spaced with an 8px gap.
Lined
Hairline dividers without card chrome — useful under a section heading (FAQ).
Frequently asked questions
Stacked
Joined group with shared borders. First/last corners are handled automatically — no position prop.
Icon & badge
Optional leading icon and trailing badge before the chevron. Until EDS ships icons, demos use Quix.
Controlled
Drive open items with value / onValueChange. Values are string arrays.
Open: one
Disabled
Disable the whole accordion or a single item.
RTL
The trigger is a horizontal flip of LTR: leading icon + title + badge on the inline-start edge, chevron on the inline-end (visually left in RTL).
API Reference
Built on Base UI Accordion.
Accordion
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "contained" | "lined" | "stacked" | "contained" | Visual treatment from Figma |
multiple | boolean | false | Allow more than one item open |
value | string[] | — | Controlled open item values |
defaultValue | string[] | — | Uncontrolled initial open values |
onValueChange | (value: string[], eventDetails) => void | — | Fires when open set changes |
disabled | boolean | false | Disables every item |
keepMounted | boolean | false | Keep closed panels in the DOM |
hiddenUntilFound | boolean | false | Expose closed panels to find-in-page |
className | string | — | Extra classes on the root |
id | string | — | Root id |
aria-label | string | — | Accessible name for the group |
aria-labelledby | string | — | Id of a visible label / heading |
Accordion.Item
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | auto | Unique value for open state |
disabled | boolean | false | Disables this item only |
onOpenChange | (open: boolean, eventDetails) => void | — | Per-item open callback |
className | string | — | Extra classes on the item |
Accordion.Trigger
Renders an <h3> header wrapping the expand/collapse <button>. Chevron chrome is built in.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Title content |
icon | ReactNode | — | Leading decorative icon (aria-hidden wrapper) |
badge | ReactNode | — | Trailing badge before the chevron |
className | string | — | Extra classes on the button |
id | string | — | Trigger id |
Accordion.Panel
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Panel body (content slot) |
className | string | — | Extra classes on the panel |
keepMounted | boolean | inherits | Override root keepMounted |
hiddenUntilFound | boolean | inherits | Override root hiddenUntilFound |
Accessibility
WCAG success criteria
| Criterion | Level | How EDS helps | |
|---|---|---|---|
| 2.1.1 Keyboard | A | Triggers are native buttons; Space/Enter toggles | |
| 2.4.7 Focus Visible | AA | Item-level focus ring via :focus-visible | |
| 4.1.2 Name, Role, Value | A | aria-expanded, header/button pairing, panel association from Base UI | |
| 1.3.1 Info and Relationships | A | Heading (h3) wraps each trigger; panels are labeled regions | |
| 1.4.3 Contrast (Minimum) | AA | Title/body use semantic text tokens | |
| 1.4.11 Non-text Contrast | AA | Borders, chevron, and focus outline use outline/border tokens | |
| 2.4.3 Focus Order | A | DOM order matches visual order; respects dir | |
| 2.2.2 Pause, Stop, Hide | A | Height animation respects prefers-reduced-motion |
Why it matters
Accordions hide content by default. Without a real button, aria-expanded, and a heading structure, keyboard and screen-reader users cannot predict what will open, whether content is already visible, or how sections relate. Clear triggers and panels keep long FAQs and settings groups scannable without trapping focus or dumping every panel into the tab order at once.
Keyboard
| Key | Behavior |
|---|---|
| ⇥ Tab / ⇧ + ⇥ Tab | Moves focus between triggers (and into focusable content inside an open panel) |
| Space / ↵ Enter | Expands or collapses the focused item |
| ⎋ Escape | Does not auto-close (APG pattern for disclosure-style accordion) |
Arrow-key roving focus is not used (aligned with current APG guidance and Base UI).
Screen readers & semantics
Built on Base UI Accordion:
| Surface | Implementation |
|---|---|
| Trigger | Native <button> with aria-expanded and aria-controls |
| Header | <h3> via Accordion.Trigger (adjust page heading levels so this nests correctly) |
| Panel | Region associated with its trigger; unmounted when closed unless keepMounted / hiddenUntilFound |
| Icon / chevron | Decorative wrappers use aria-hidden="true" |
| Badge | Announced as part of the button name when it is text |
Prefer a page heading + aria-labelledby on the root when a visible “FAQ” title already exists. Use hiddenUntilFound if closed answers must appear in browser find-in-page.
Focus & visuals
| Concern | EDS behavior |
|---|---|
| Focus ring | 2px outline-primary on the item when the trigger is :focus-visible |
| Expanded | Chevron rotates 180°; Contained/Stacked show a header/content separator |
| Hover | adaptive-8 wash on the trigger |
| Disabled | text-disabled; trigger is not activatable |
| RTL | Inherits ancestor dir via Base UI DirectionProvider — full horizontal flip of the trigger |
| Motion | Panel height animates; disabled under prefers-reduced-motion |
Build with it
| Do | Don’t | ||
|---|---|---|---|
Put meaningful titles in Accordion.Trigger | Use vague labels like “Click here” or duplicate titles | ||
| Keep panel content short and scannable | Bury entire multi-step forms in a single panel | ||
Use variant="lined" under a section heading for FAQs | Nest accordions inside accordion panels without strong need | ||
Mark decorative icons aria-hidden (EDS wraps icon) | Put interactive controls inside the trigger | ||
Choose multiple intentionally for the task | Force exclusive mode when users need to compare open sections |
Quick test
- Keyboard — Tab through triggers, toggle with Space/Enter, confirm focus can enter open panel content.
- Screen reader — Verify expanded/collapsed state and that panel content is announced when opened.
- Find-in-page — With
hiddenUntilFound, search for text in a closed panel and confirm it expands. - RTL — Open the RTL example; confirm the row is mirrored (chevron on the left, title/icon on the right).
- Motion — Enable reduced motion and confirm panels open without height animation.