Overview
The Accordion (also called a collapsible, expandable panel, or disclosure widget) is a vertically stacked set of interactive headings that each reveal or hide an associated section of content. Accordions are one of the most effective tools for managing information density — they let users see all available topics at a glance while keeping the page scannable.
The core interaction is simple: click a heading, its content expands; click again (or click another heading), it collapses. But the design decisions underneath — single vs. multiple expansion, animation behavior, initial state, nested accordions — significantly impact usability.
When to use an Accordion:
- FAQ sections — The canonical accordion use case. Users scan questions and expand only what's relevant.
- Settings or configuration panels — Group related settings into collapsible sections.
- Progressive disclosure — Show summary information with details available on demand.
- Long-form content on mobile — Condense content that would require excessive scrolling.
- Sidebar filters — Collapsible filter groups (Price, Brand, Size) in e-commerce interfaces.
When NOT to use an Accordion:
- When most users need to see all content — just show it inline. Accordions add clicks.
- For navigation between views — use Tabs. Accordions reveal content within the same flow; tabs switch between views.
- For very short content (1–2 sentences per section) — the click-to-reveal overhead isn't worth it. Just show everything.
- When users need to compare content across sections simultaneously — they can't see two accordion panels at once (unless you use multi-expand mode).
Accordion vs. Tabs decision: If users typically need to see only one section at a time and the sections are sequential, use an Accordion. If users switch back and forth between a small number (2–5) of equally important views, use Tabs.
Animate your expand/collapse with the Transition Generator and Animation Generator. Both tools let you preview easing curves that feel natural for content reveals.
Variants
Behavior Variants
| Variant | Description | Use Case |
|---|---|---|
| Single expand | Only one section can be open at a time. Opening one closes the previously open section. | FAQs, mobile settings, constrained vertical space |
| Multi expand | Multiple sections can be open simultaneously. Each toggles independently. | Desktop settings, filter panels, reference documentation |
| Always one open | Like single expand, but at least one section must remain open — the user can't collapse all. | When a "no content visible" state is confusing |
Visual Variants
| Variant | Description |
|---|---|
| Bordered | Each section has a visible border. Sections are separated by gaps. |
| Flush | No outer borders. Sections separated only by divider lines. Minimal look. |
| Contained | All sections share a single container with internal dividers. |
| Card-style | Each section is a separate Card. Elevated with shadow. |
Chevron / Indicator Position
| Position | Behavior |
|---|---|
| Right (default) | Chevron on the far right side. Rotates 180° on expand. |
| Left | Chevron on the far left, before the heading text. Common in tree views. |
| Plus/Minus | + when collapsed, − when expanded. Classic FAQ style. |
| None | No visual indicator — rely on content visibility + heading styling. Risky for discoverability. |
Size Variants
| Size | Header Height | Header Font Size | Use Case |
|---|---|---|---|
| Small | 40px | 14px | Sidebar filters, nested accordions |
| Medium | 48px | 16px | Default content sections |
| Large | 56px | 18px | Landing page FAQs, hero-adjacent content |
Properties
Accordion Properties
| Property | Type | Default | Description |
|---|---|---|---|
type | 'single' | 'multiple' | 'single' | Whether one or multiple sections can be open |
value | string | string[] | — | Controlled open section(s). String for single, array for multiple. |
defaultValue | string | string[] | — | Initially open section(s) (uncontrolled) |
onValueChange | (value: string | string[]) => void | — | Callback when open sections change |
collapsible | boolean | true | Whether all sections can be collapsed (single mode). Set false to enforce always-one-open. |
disabled | boolean | false | Disables all sections |
Accordion Item Properties
| Property | Type | Default | Description |
|---|---|---|---|
value | string | — | Unique identifier for this section |
disabled | boolean | false | Disables this specific section |
trigger | ReactNode | — | Header content (text, icon, badge, etc.) |
children | ReactNode | — | Collapsible body content |
Token Mappings
Design Token Mappings
| Token Category | Token Example | Accordion Usage |
|---|---|---|
| Color – Header Background | --color-surface or --color-surface-secondary | Accordion trigger background |
| Color – Header Hover | --color-surface-hover | Trigger background on hover |
| Color – Content Background | --color-surface | Expanded panel body |
| Color – Border | --color-border-subtle | Dividers between sections |
| Color – Text | --color-text-primary | Trigger heading text |
| Color – Icon | --color-text-secondary | Chevron/indicator icon |
| Border Radius | --radius-lg (12px) | Outer container corners (contained variant) |
| Spacing – Header Padding | --space-4 (16px) | Horizontal padding in the trigger |
| Spacing – Content Padding | --space-4 to --space-6 | Body content padding |
| Typography | --font-weight-medium, --font-size-md | Trigger heading |
| Transition | --duration-normal (200ms), --ease-out | Expand/collapse animation. Fine-tune with Transition Generator. |
| Transition – Chevron | --duration-fast (150ms) | Chevron rotation |
For full token documentation, see our Design Tokens Complete Guide.
States
Accordion States
| State | Visual Treatment |
|---|---|
| All collapsed | All sections show only their trigger headers. Default initial state (unless defaultValue is set). |
| Section expanded | Active section's content is visible. Chevron rotated. Trigger may have an active background or bold text. |
| Expanding (animating) | Content height animates from 0 to auto. Use grid-template-rows: 0fr → 1fr or max-height animation. |
| Collapsing (animating) | Reverse of expanding. Content height animates to 0, then the section is hidden. |
| Hover | Trigger background subtly changes. Cursor: pointer. |
| Focus | Visible focus ring on the trigger button. Must meet 3:1 contrast (WCAG SC 1.4.11). |
| Disabled | Reduced opacity (0.5). Trigger is not interactive. cursor: not-allowed. |
| Disabled + Expanded | Content remains visible but the trigger can't be toggled. Rare — usually disable means collapse. |
Expand/Collapse Animation
The smoothest accordion animation uses CSS Grid:
.accordion-content {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 200ms ease-out;
}
.accordion-content[data-state="open"] {
grid-template-rows: 1fr;
}
.accordion-content > div {
overflow: hidden;
}
This is superior to max-height (which requires a fixed pixel value and often overshoots) and JavaScript height calculation (which causes layout thrash). Preview timing values with our Animation Generator.
Accessibility
WCAG Requirements
| Criterion | Level | Requirement |
|---|---|---|
| SC 1.3.1 Info and Relationships | A | Heading level of triggers must reflect the page hierarchy. Use <h3> inside an <h2> section, etc. |
| SC 1.4.3 Contrast (Minimum) | AA | Trigger text: 4.5:1 contrast. Icons (chevron): 3:1 per SC 1.4.11. Validate with Contrast Checker. |
| SC 2.1.1 Keyboard | A | All triggers must be operable via keyboard. |
| SC 2.4.6 Headings and Labels | AA | Accordion triggers should be descriptive headings that make sense out of context. |
| SC 4.1.2 Name, Role, Value | A | Triggers must expose expanded/collapsed state to assistive technology. |
ARIA Pattern: Disclosure (Recommended)
The W3C WAI-ARIA Authoring Practices recommend the disclosure pattern for accordions. Each trigger is a <button> inside a heading element:
- Trigger:
<h3><button aria-expanded="true|false" aria-controls="panel-id">Section Title</button></h3> - Panel:
<div id="panel-id" role="region" aria-labelledby="trigger-id">(userole="region"only if there are 6 or fewer sections — too many regions pollute the landmark list) - No
role="tablist": Accordions are NOT tabs. Don't use tab/tabpanel roles for accordions — the keyboard interaction model is completely different.
Keyboard Interaction
| Key | Action |
|---|---|
| Enter / Space | Toggle the focused accordion trigger (expand/collapse) |
| Tab | Move focus to the next focusable element (next trigger or into expanded content) |
| Shift + Tab | Move focus to the previous focusable element |
| Arrow Down (optional) | Move focus to the next accordion trigger |
| Arrow Up (optional) | Move focus to the previous accordion trigger |
| Home (optional) | Move focus to the first accordion trigger |
| End (optional) | Move focus to the last accordion trigger |
Arrow key navigation between triggers is recommended but optional. Tab navigation alone is sufficient for compliance. If you implement arrow keys, the triggers should be managed as a single tab stop (roving tabindex).
Screen Reader Announcements
When a user focuses a trigger, the screen reader announces: "[Heading level], [label], button, [collapsed/expanded]." This tells the user exactly what they're on and its current state — no additional ARIA needed beyond aria-expanded.
For more patterns, see our ARIA Attributes Guide and Keyboard Accessibility Guide.
Usage Guidelines
Do's
- ✅ Use descriptive trigger labels that make sense without seeing the content. "What is your return policy?" is better than "Returns."
- ✅ Default to all-collapsed on page load for FAQ sections. Users should scan headings first and choose what to expand.
- ✅ Consider default-expanded for the first section when there's a clear "most asked" question or primary setting.
- ✅ Use single-expand mode on mobile to conserve vertical space and prevent excessive scrolling.
- ✅ Animate the transition — a 200ms ease-out gives clear feedback about what opened/closed. Instant show/hide is disorienting.
- ✅ Allow deep linking to specific sections via URL hash (
#faq-returns). Auto-expand and scroll to that section on load.
Don'ts
- ❌ Don't nest accordions more than one level deep. If you need sub-accordions inside accordions, your information architecture needs rethinking.
- ❌ Don't put essential content behind accordions. If every user needs to see it, don't hide it behind a click.
- ❌ Don't use an accordion with only one section. That's just a disclosure widget — semantically fine, but "accordion" implies multiple panels.
- ❌ Don't animate with
max-height: 9999px— the timing will be wrong because CSS transitions interpolate the full value. Use CSS Grid or JavaScript height calculation. - ❌ Don't use accordions for sequential steps. If order matters and each step depends on the previous, use a Stepper.
- ❌ Don't disable all sections. If no section is interactive, remove the accordion entirely and show the content flat.
SEO Considerations
Content inside collapsed accordion panels is in the DOM and is indexed by search engines. Google has confirmed this. However, content that requires interaction to become visible may receive slightly lower ranking weight. For critical SEO content, consider showing it by default (server-rendered, expanded) and letting JavaScript add the collapse behavior.
Code Snippets
<!-- Accordion using native <details> + <summary> -->
<div class="accordion">
<details class="accordion-item" open>
<summary class="accordion-trigger">
<h3>What is your return policy?</h3>
<svg class="chevron" aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
<path d="M5.47 7.47a.75.75 0 0 1 1.06 0L10 10.94l3.47-3.47a.75.75 0 1 1 1.06 1.06l-4 4a.75.75 0 0 1-1.06 0l-4-4a.75.75 0 0 1 0-1.06Z"/>
</svg>
</summary>
<div class="accordion-content">
<p>You can return any item within 30 days of purchase for a full refund.
Items must be in their original packaging.</p>
</div>
</details>
<details class="accordion-item">
<summary class="accordion-trigger">
<h3>How long does shipping take?</h3>
<svg class="chevron" aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
<path d="M5.47 7.47a.75.75 0 0 1 1.06 0L10 10.94l3.47-3.47a.75.75 0 1 1 1.06 1.06l-4 4a.75.75 0 0 1-1.06 0l-4-4a.75.75 0 0 1 0-1.06Z"/>
</svg>
</summary>
<div class="accordion-content">
<p>Standard shipping takes 5-7 business days. Express shipping (2-3 days)
is available at checkout for an additional fee.</p>
</div>
</details>
<details class="accordion-item">
<summary class="accordion-trigger">
<h3>Do you offer international shipping?</h3>
<svg class="chevron" aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
<path d="M5.47 7.47a.75.75 0 0 1 1.06 0L10 10.94l3.47-3.47a.75.75 0 1 1 1.06 1.06l-4 4a.75.75 0 0 1-1.06 0l-4-4a.75.75 0 0 1 0-1.06Z"/>
</svg>
</summary>
<div class="accordion-content">
<p>Yes, we ship to over 40 countries. International delivery typically
takes 10-14 business days depending on your location.</p>
</div>
</details>
</div>
<!-- ARIA-enhanced version for custom accordion behavior -->
<div class="accordion" role="presentation">
<h3>
<button
type="button"
class="accordion-trigger"
aria-expanded="true"
aria-controls="panel-1"
id="trigger-1"
>
What is your return policy?
<span class="chevron" aria-hidden="true">▾</span>
</button>
</h3>
<div
id="panel-1"
role="region"
aria-labelledby="trigger-1"
class="accordion-content"
>
<p>You can return any item within 30 days of purchase.</p>
</div>
</div>import { useState, useCallback, type ReactNode } from "react";
interface AccordionItem {
value: string;
trigger: ReactNode;
content: ReactNode;
disabled?: boolean;
}
interface AccordionProps {
items: AccordionItem[];
type?: "single" | "multiple";
defaultValue?: string[];
collapsible?: boolean;
}
export default function Accordion({
items,
type = "single",
defaultValue = [],
collapsible = true,
}: AccordionProps) {
const [openItems, setOpenItems] = useState<string[]>(defaultValue);
const toggle = useCallback(
(value: string) => {
setOpenItems((prev) => {
const isOpen = prev.includes(value);
if (type === "single") {
if (isOpen && collapsible) return [];
if (isOpen && !collapsible) return prev;
return [value];
}
// multiple
return isOpen ? prev.filter((v) => v !== value) : [...prev, value];
});
},
[type, collapsible],
);
return (
<div className="accordion">
{items.map((item) => {
const isOpen = openItems.includes(item.value);
const triggerId = `accordion-trigger-${item.value}`;
const panelId = `accordion-panel-${item.value}`;
return (
<div key={item.value} className="accordion-item" data-state={isOpen ? "open" : "closed"}>
<h3>
<button
type="button"
id={triggerId}
className="accordion-trigger"
aria-expanded={isOpen}
aria-controls={panelId}
disabled={item.disabled}
onClick={() => toggle(item.value)}
>
{item.trigger}
<svg
className={`chevron ${isOpen ? "rotated" : ""}`}
aria-hidden="true"
width="20"
height="20"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M5.47 7.47a.75.75 0 0 1 1.06 0L10 10.94l3.47-3.47a.75.75 0 1 1 1.06 1.06l-4 4a.75.75 0 0 1-1.06 0l-4-4a.75.75 0 0 1 0-1.06Z" />
</svg>
</button>
</h3>
<div
id={panelId}
role="region"
aria-labelledby={triggerId}
className="accordion-content"
data-state={isOpen ? "open" : "closed"}
>
<div className="accordion-content-inner">{item.content}</div>
</div>
</div>
);
})}
</div>
);
}
// Usage
<Accordion
type="single"
defaultValue={["returns"]}
items={[
{
value: "returns",
trigger: "What is your return policy?",
content: <p>Return any item within 30 days for a full refund.</p>,
},
{
value: "shipping",
trigger: "How long does shipping take?",
content: <p>Standard: 5-7 business days. Express: 2-3 days.</p>,
},
{
value: "international",
trigger: "Do you offer international shipping?",
content: <p>Yes — we ship to 40+ countries worldwide.</p>,
},
]}
/>Design Systems
Cross-System Comparison
| Feature | Material 3 | Shadcn/ui | Radix | Ant Design |
|---|---|---|---|---|
| Component | Expansion Panel (deprecated) → now use lists | Accordion (wraps Radix) | Accordion primitive | Collapse / Collapse.Panel |
| Single/Multiple | Not built-in (manual) | type="single" | "multiple" via Radix | type="single" | "multiple" | accordion prop (boolean) |
| Animation | Material Motion (height + fade) | CSS Grid animation (Tailwind) | BYO (CSS or Framer Motion) | Built-in slide animation |
| Collapsible | N/A | collapsible prop | collapsible prop (single mode) | All collapsed allowed by default |
| Nested | Discouraged | Technically works | Supported (nested primitives) | Supported |
| Disabled items | Per-panel | disabled on AccordionItem | disabled on Item | collapsible={false} per panel |
| Keyboard | Enter/Space | Full (Radix: arrows, Home, End) | Full ARIA pattern | Enter/Space |
| Custom trigger | Full control | AccordionTrigger slot | Trigger slot pattern | header prop or extra slot |
Notable Approaches
Radix Accordion is the most complete primitive available. It supports single/multiple expansion modes, collapsible control, disabled items, roving tabindex with arrow key navigation, and the full WAI-ARIA disclosure pattern. The animation model uses data-state="open|closed" attributes and exposes a --radix-accordion-content-height CSS variable for animating content height — solving the "animate to auto height" problem elegantly.
Material 3 effectively deprecated the Expansion Panel in favor of recommending list items with expandable content. Their guidance recognizes that most accordion use cases are better served by other patterns (tabs for navigation, lists for progressive disclosure). When you do need an accordion in Material, you build it from ListItem components.
Ant Design Collapse offers a destroyInactivePanel prop that removes collapsed panel content from the DOM — useful for performance when panels contain heavy content (charts, maps, data grids). It also supports a ghost variant that removes all borders and backgrounds for a minimal look.
Native HTML <details>/<summary> provides a zero-JavaScript accordion. It's accessible by default, supports the open attribute, and works everywhere. The downsides: no native animation support (though the ::details-content pseudo-element is emerging in 2026), no single-expand mode (all <details> elements are independent), and limited styling control. For simple FAQ sections where JavaScript isn't wanted, it's perfect.
Preview your expand/collapse timing curves with our Animation Generator — a 200ms ease-out is a solid starting point.