Component · Containers & Overlays
Accordion
Overview
Progressive disclosure for stacked sections — each item's content lives in the page flow, and expanding one doesn't remove the others from view. This is the core distinction from Tabs: Accordion items stack and coexist; Tabs panels replace each other in a single fixed region — see Accordion vs. Tabs below. Use Accordion for a list of collapsible sections (an FAQ, a settings group); use Tabs for parallel views of one subject.
Anatomy
Accordion (group) → Item → Trigger (label + chevron icon) → Content panel.
- Accordion — the group wrapper; owns single-open vs. multi-open behavior (below).
- Item — one collapsible unit: a Trigger plus its Content panel.
- Trigger — a full-width native
<button>, wrapped in a heading element (see Accessibility), containing the item's label and a trailing chevron icon that rotates 180° between collapsed and expanded. - Content panel — the item's body, hidden when collapsed, visible when expanded.
Live Example
Single-open (default) — an FAQ
Multi-open — independent settings groups
Code Example
"use client";
import { Button } from "@/components/ui/button";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
/**
* Live Accordion example — each Trigger is a full-width native `<button>`
* wrapped in a heading (`<h3>` here), an ordinary tab stop: `Tab` / `Shift+Tab`
* reach every enabled Trigger directly, `Enter` / `Space` toggle the focused
* one, and `Up` / `Down` / `Home` / `End` move focus between Triggers as an
* additive enhancement. The chevron rotates 180° when expanded.
*
* The first group is single-open (opening one collapses the others); the second
* is multi-open and shows a per-item `disabled` Trigger plus a secondary header
* action as a SIBLING of the Trigger button, never nested inside it.
*/
export function AccordionShowcase() {
return (
<div className="flex flex-col gap-8">
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Single-open (default) — an FAQ
</h3>
<Accordion defaultValue="ship">
<AccordionItem value="ship">
<AccordionTrigger>How long does shipping take?</AccordionTrigger>
<AccordionContent>
Standard shipping is 3–5 business days. Express is 1–2 business
days and is calculated at checkout.
</AccordionContent>
</AccordionItem>
<AccordionItem value="returns">
<AccordionTrigger>What is the return policy?</AccordionTrigger>
<AccordionContent>
Unused items can be returned within 30 days for a full refund.
Opening one answer collapses any other open answer.
</AccordionContent>
</AccordionItem>
<AccordionItem value="track">
<AccordionTrigger>Can I track my order?</AccordionTrigger>
<AccordionContent>
Yes — a tracking link is emailed as soon as the order ships.
</AccordionContent>
</AccordionItem>
</Accordion>
</section>
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Multi-open — independent settings groups
</h3>
<Accordion type="multiple" defaultValue={["profile"]}>
<AccordionItem value="profile">
{/* Secondary action sits beside the Trigger, not inside it. */}
<div className="flex items-center">
<AccordionTrigger>Profile</AccordionTrigger>
<Button variant="ghost" size="sm" className="mr-2 shrink-0">
Edit
</Button>
</div>
<AccordionContent>
Display name, avatar, and public bio.
</AccordionContent>
</AccordionItem>
<AccordionItem value="notifications">
<AccordionTrigger>Notifications</AccordionTrigger>
<AccordionContent>
Email, push, and digest frequency. Each group here toggles
independently of the others.
</AccordionContent>
</AccordionItem>
<AccordionItem value="danger" disabled>
<AccordionTrigger>Danger zone (disabled)</AccordionTrigger>
<AccordionContent>
This Trigger is disabled and skipped by keyboard navigation.
</AccordionContent>
</AccordionItem>
</Accordion>
</section>
</div>
);
}States
| State | Look |
|---|---|
| Collapsed | Chevron points down (or right, in a right-chevron layout), panel hidden |
| Expanded | Chevron rotated 180°, panel visible |
| Hover | Trigger row background shifts to --accent |
| Focus-visible | --ring on :focus-visible (keyboard focus only) — same --ring token and :focus-visible trigger as Button, drawn flush against the trigger (the offset geometry Button ships is not reused here) — see Color — Border width |
| Disabled (per-item) | Reduced opacity (opacity-50; Color — Disabled dimming), trigger disabled, removed from keyboard navigation |
Usage Guidance
Accordion vs. Tabs
| Accordion | Tabs | |
|---|---|---|
| Content relationship | Stacks — multiple items can be open at once, in document flow | Replaces — exactly one panel visible, others hidden |
| Typical use | FAQ, collapsible settings groups, long-form disclosure | Alternate views of one record/subject |
| Trigger | Full-width row, expand/collapse | Short label in a horizontal list |
Single-open vs. multi-open
| Mode | Behavior | Use for |
|---|---|---|
| Single-open (default) | Expanding one item collapses any other open item — at most one item open at a time | A list where items are alternatives to each other (an FAQ where one answer at a time keeps the page scannable) |
| Multi-open | Each item expands and collapses independently | A list where items are genuinely independent (a settings page with unrelated collapsible groups) |
Pick per instance, not globally — a component prop, not a system-wide rule.
Long content
A panel grows to fit its content; there's no internal max-height or scroll baked into Accordion itself (unlike Dialog or Sheet, which bound an overlay's height on purpose). If a panel's content is long enough to need its own scroll region, that's a decision made by the content inside it, not by Accordion.
Nested interactive content
A panel may contain further interactive controls (buttons, links, form fields) — the panel is
ordinary content, not a constrained container. The one hard constraint is on the Trigger row
itself: because the Trigger is a native <button>, nothing else interactive can be nested inside
that same button (invalid HTML — a <button> can't contain another focusable control). A
secondary action on the header row (e.g. a "remove section" icon-button) must sit as a sibling
next to the Trigger, not inside it — the same class of problem
Checkbox already documents for a label with a nested link.
Avoid deep nesting
Don't nest a full Accordion inside another Accordion item's content. Two or more levels of nested disclosure is hard to scan and harder to navigate by keyboard or screen reader — flatten the hierarchy (a longer single-level list, or split into a separate page/section) instead of stacking Accordions.
Tokens
Accordion uses existing Monogem semantic tokens to stay consistent with the design system; it introduces no component-specific styling values.
| Token | Where used |
|---|---|
--foreground | Trigger label text |
--muted-foreground | Disabled trigger text |
--accent | Trigger hover background |
--border | Divider between stacked items |
--ring | Focus-visible ring |
--scale-4 | Trigger padding, panel padding |
--scale-4 | Chevron icon size — Icons's "default UI" bucket |
| Label / button text recipe | Trigger text, same as Button |
| Body small text recipe | Panel content default text size |
Do / Don’t
Do
- Default to single-open for lists of alternative answers; use multi-open only when items are genuinely independent.
- Keep trigger labels short — a question or a short phrase, scannable at a glance.
- Place a secondary header action as a sibling of the Trigger button, never nested inside it.
Don’t
- Nest an Accordion inside another Accordion's content — flatten the structure instead.
- Use Accordion to fake Tabs (forcing single-open and styling it to look like a tab switch) — reach for Tabs directly when the content is genuinely a parallel view, not a stacked disclosure.
- Cap a panel's height with a forced internal scroll by default — that's a per-content decision, not part of the baseline.
Accessibility
- Each Trigger is a native
<button>wrapped in a heading element (<h3>by default, or whatever level is correct for the page's outline — the same contextual-heading-level rule Card uses). - Trigger sets
aria-expanded(true/false) andaria-controlspointing at its panel'sid. - The panel gets an
idandaria-labelledbypointing back at its trigger. Arole="region"on the panel is added only when there are few enough items that each genuinely earns a landmark a screen-reader user might jump to directly — on a long list, addingrole="region"to every item creates landmark noise rather than useful navigation; the same "semantic only when it earns it" call Separator makes. - Keyboard:
Enter/Spacetoggles the focused Trigger. Each Trigger is an ordinary tab stop — see Tab-stop model — soTab/Shift+Tabreach every enabled Trigger directly; optionalUp/Down/Home/Endarrow-key navigation between Triggers may be added on top of, not instead of, that ordinary tab order. - Expanded state is never color-only — the chevron rotation and the panel's presence/absence are the primary signals.
Tab-stop model
Accordion does not use Tabs' roving-tabindex pattern. Each
Trigger is an ordinary, independently focusable native <button>:
- Every enabled Trigger remains in the page's normal tab sequence — no
tabindex="-1"is applied to any of them. Tab/Shift+Tabmove through Triggers and any other interactive page content in document order, exactly as they would for any other button on the page.Enter/Spaceexpands or collapses whichever Trigger currently has focus.- Optional
Up/Down/Home/Endarrow-key navigation between Triggers may be added as an enhancement, but it's additive — a keyboard user must still be able toTabdirectly to any enabled Trigger, not only arrow into it. - Disabled Triggers are not interactive and follow the same disabled-state contract as any other disabled control — removed from the tab sequence.
Related Components / Patterns
- Tabs — switches between views of equal standing instead of expanding sections in place.