Skip to content
Monogem

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

Standard shipping is 3–5 business days. Express is 1–2 business days and is calculated at checkout.

Multi-open — independent settings groups

Display name, avatar, and public bio.

Code Example

accordion-showcase.tsx
"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

StateLook
CollapsedChevron points down (or right, in a right-chevron layout), panel hidden
ExpandedChevron rotated 180°, panel visible
HoverTrigger 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

AccordionTabs
Content relationshipStacks — multiple items can be open at once, in document flowReplaces — exactly one panel visible, others hidden
Typical useFAQ, collapsible settings groups, long-form disclosureAlternate views of one record/subject
TriggerFull-width row, expand/collapseShort label in a horizontal list

Single-open vs. multi-open

ModeBehaviorUse for
Single-open (default)Expanding one item collapses any other open item — at most one item open at a timeA list where items are alternatives to each other (an FAQ where one answer at a time keeps the page scannable)
Multi-openEach item expands and collapses independentlyA 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.

TokenWhere used
--foregroundTrigger label text
--muted-foregroundDisabled trigger text
--accentTrigger hover background
--borderDivider between stacked items
--ringFocus-visible ring
--scale-4Trigger padding, panel padding
--scale-4Chevron icon size — Icons's "default UI" bucket
Label / button text recipeTrigger text, same as Button
Body small text recipePanel 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) and aria-controls pointing at its panel's id.
  • The panel gets an id and aria-labelledby pointing back at its trigger. A role="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, adding role="region" to every item creates landmark noise rather than useful navigation; the same "semantic only when it earns it" call Separator makes.
  • Keyboard: Enter/Space toggles the focused Trigger. Each Trigger is an ordinary tab stop — see Tab-stop model — so Tab/Shift+Tab reach every enabled Trigger directly; optional Up/Down/Home/End arrow-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+Tab move through Triggers and any other interactive page content in document order, exactly as they would for any other button on the page.
  • Enter / Space expands or collapses whichever Trigger currently has focus.
  • Optional Up/Down/Home/End arrow-key navigation between Triggers may be added as an enhancement, but it's additive — a keyboard user must still be able to Tab directly 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.