Loading…
Loading…
Vertically stacked sections that expand and collapse to reveal content.
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:
When NOT to use an Accordion:
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.
| 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 |
| 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. |
| 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 | 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 |
| 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 |
| 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 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.
| 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. |
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.
| 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. |
The W3C WAI-ARIA Authoring Practices recommend the disclosure pattern for accordions. Each trigger is a <button> inside a heading element:
<h3><button aria-expanded="true|false" aria-controls="panel-id">Section Title</button></h3><div id="panel-id" role="region" aria-labelledby="trigger-id"> (use role="region" only if there are 6 or fewer sections — too many regions pollute the landmark list)role="tablist": Accordions are NOT tabs. Don't use tab/tabpanel roles for accordions — the keyboard interaction model is completely different.| 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).
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.
#faq-returns). Auto-expand and scroll to that section on load.max-height: 9999px — the timing will be wrong because CSS transitions interpolate the full value. Use CSS Grid or JavaScript height calculation.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.
<!-- 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>,
},
]}
/>| 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 |
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.