---
title: "Accordion"
description: "Sections that expand and collapse, one or several at a time, built on Base UI Accordion."
url: "https://opsinjs.pensievelabs.org/components/accordion"
source: "https://opsinjs.pensievelabs.org/components/accordion.md"
section: "Components"
status: "shipped"
kind: "component"
category: "navigation"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["disclosure", "expander", "collapsible"]
usedIn: ["diabetes-medicines-app"]
implemented: true
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> `<StubNotice>` IS THE EXCEPTION, AND IT IS THE ONE TO READ. It is a
> paired element rather than a self-closing one, and the text between
> its opening and closing tags is prose an author wrote, reproduced
> below word for word. That prose is where this page says whether the
> component has been reviewed. Read the children, not only the
> attributes.

<StubNotice
  name="accordion"
  status="shipped"
  questions="[
  &#x22;Nothing a reader must not miss may live in a collapsed panel, but the wrapper cannot read its content, so a folded safety message passes every automated gate.&#x22;,
  &#x22;Nobody has listened to it in a screen reader, so whether a section opens cleanly and the h3 level suits the host page is unheard.&#x22;,
  &#x22;No contrast pair has been measured, and forced-colors, prefers-contrast: more and the panel's reduced-motion mount are untested beyond the product stylesheet.&#x22;,
]"
>
  Audited against WCAG 2.2 AA. Clinical review pending.
</StubNotice>

## Preview [#preview]

<ComponentPreview name="accordion" />

## Installation [#installation]

<ComponentInstall name="accordion" unbuilt="false" importPath="@/components/ui/accordion" dependencies="[&#x22;@base-ui/react&#x22;, &#x22;lucide-react&#x22;]" />

## Usage [#usage]

```tsx
import { Accordion } from "@/components/ui/accordion"
```

```tsx
<Accordion
  items={[
    { value: "hours", title: "When can I reach the team?", content: "Weekday mornings." },
    { value: "records", title: "Where are my notes kept?", content: "On your device." },
  ]}
/>
```

## When to use it [#when-to-use-it]

<WhenToUse
  use="[
  &#x22;An index of related sections a reader scans first and opens one at a time, such as questions and answers.&#x22;,
  &#x22;Detail a reader wants on demand but not on arrival, such as small print under a summary.&#x22;,
  &#x22;Several independent groups of settings on one small screen, opened together with multiple.&#x22;,
]"
  avoid="[
  { case: &#x22;A safety message, an urgent instruction or anything a reader must not miss. A collapsed panel may never be opened.&#x22;, instead: &#x22;callout&#x22; },
  { case: &#x22;Content that must always be visible, not behind a header a reader has to press.&#x22;, instead: &#x22;card&#x22; },
  { case: &#x22;Choosing a header should swap the whole view for another in the same space, not reveal detail in place.&#x22;, instead: &#x22;tabs&#x22; },
]"
/>

## Anatomy [#anatomy]

<Anatomy
  name="accordion"
  parts="[
  {
    name: &#x22;Accordion&#x22;,
    describes: &#x22;Base UI's Accordion.Root: one card surface with a hairline that manages open state. Neutral chrome, neither colour axis.&#x22;,
    prop: &#x22;items&#x22;,
  },
  {
    name: &#x22;Accordion.Item&#x22;,
    describes: &#x22;One section, a header with its panel. A top hairline divides it from the section above.&#x22;,
    prop: &#x22;items&#x22;,
  },
  {
    name: &#x22;Accordion.Header&#x22;,
    describes: &#x22;The section's heading, rendered by Base UI as an h3 so it sits in the document outline.&#x22;,
  },
  {
    name: &#x22;Accordion.Trigger&#x22;,
    describes: &#x22;The full-width button that toggles the section, with aria-expanded, aria-controls, a 44pt target floor, the title and chevron.&#x22;,
    prop: &#x22;value&#x22;,
  },
  {
    name: &#x22;Accordion.Panel&#x22;,
    describes: &#x22;The revealed content, unmounted while closed, so a shut panel costs nothing and leaves the tab order.&#x22;,
    prop: &#x22;content&#x22;,
  },
]"
/>

<CompositionTree
  name="accordion"
  tree="[
  {
    part: &#x22;Accordion&#x22;,
    cardinality: &#x22;1&#x22;,
    children: [
      {
        part: &#x22;Accordion.Item&#x22;,
        cardinality: &#x22;1..n&#x22;,
        children: [
          {
            part: &#x22;Accordion.Header&#x22;,
            cardinality: &#x22;1&#x22;,
            children: [
              {
                part: &#x22;Accordion.Trigger&#x22;,
                cardinality: &#x22;1&#x22;,
              },
            ],
          },
          {
            part: &#x22;Accordion.Panel&#x22;,
            cardinality: &#x22;1&#x22;,
          },
        ],
      },
    ],
  },
]"
/>

## Examples [#examples]

### A frequently asked list [#a-frequently-asked-list]

Three headers form an index; the reader opens one. Fictional copy.

<ComponentPreview name="accordion-frequently-asked" kind="example" align="start" />

### One section open at a time [#one-section-open-at-a-time]

With `multiple` off (the default), opening one section closes the last.

<ComponentPreview name="accordion-one-open-at-a-time" kind="example" align="start" />

## Content guidelines [#content-guidelines]

Write headers as a scannable index: a question, a noun phrase or a setting name,
in sentence case, without terminal punctuation. Never fold a warning or a required
step into a panel that starts closed. It belongs in a
[Callout](./callout.mdx) that stays open.

<DoDont>
  <DoDont.Do>
    **"When can I reach the team?"** The reader's own question, short enough to
    scan against its siblings.
  </DoDont.Do>

  <DoDont.Dont>
    **A "More" panel holding "Call your clinician if this reading keeps rising."**
    An instruction the reader may never open.
  </DoDont.Dont>
</DoDont>

## Accessibility [#accessibility]

**Audited against WCAG 2.2 AA in a source pass and a rendered pass.** It is
author-run, not independent, and clinical review is pending. The rendered pass
(axe, target size, focus visible, 320px reflow) was clean.

**One defect was fixed.** The trigger's focus ring was drawn outside the trigger,
where the root's `overflow-hidden` clipped it (2.4.7). It now renders inside,
whole on every straight edge, with a small notch still clipped at the first and
last sections' outer corners, a known residual.

* Base UI renders each header as an `h3` with a `button`, wires `aria-expanded`
  and `aria-controls` between trigger and panel, and hides the chevron with
  `aria-hidden`, so a reader relies on `aria-expanded`, not the glyph.
* The chevron's turn uses `--opsin-duration-fast`, collapsed under
  `prefers-reduced-motion`. A closed panel is unmounted, leaving the tab order.

**Still unmeasured.** The rendered pass does not compute contrast, so every pair
against the card surface is unmeasured, not known-failing. That no must-see
content may sit in a closed panel stays a docs contract the wrapper cannot
enforce.

<KeyboardTable
  name="accordion"
  rows="[
  {
    keys: &#x22;Tab&#x22;,
    action: &#x22;Moves focus to the next header&#x22;,
    notes: &#x22;An ordinary tab stop. A disabled header keeps its stop.&#x22;,
  },
  {
    keys: &#x22;Shift + Tab&#x22;,
    action: &#x22;Moves focus to the previous header&#x22;,
    notes: &#x22;The same in reverse.&#x22;,
  },
  {
    keys: &#x22;Enter, Space&#x22;,
    action: &#x22;Opens or closes the focused section&#x22;,
    notes: &#x22;A native button. With multiple off, the open section closes.&#x22;,
  },
  {
    keys: &#x22;Arrow keys&#x22;,
    action: &#x22;Not bound&#x22;,
    notes: &#x22;Base UI follows the updated APG guidance, which dropped roving focus.&#x22;,
  },
]"
/>

<ContrastReport component="accordion" />

## Data attributes [#data-attributes]

<DataAttributesTable
  name="accordion"
  rows="[
  {
    attribute: &#x22;data-slot&#x22;,
    condition: &#x22;On the root and each part&#x22;,
    value: &#x22;accordion, accordion-item, accordion-header, accordion-trigger, accordion-title, accordion-panel&#x22;,
  },
  {
    attribute: &#x22;data-open&#x22;,
    condition: &#x22;On the item and the panel&#x22;,
    value: &#x22;Present while expanded&#x22;,
  },
  {
    attribute: &#x22;data-panel-open&#x22;,
    condition: &#x22;On the trigger&#x22;,
    value: &#x22;Present while its section is open&#x22;,
  },
  {
    attribute: &#x22;data-disabled&#x22;,
    condition: &#x22;On the item and its trigger&#x22;,
    value: &#x22;Present while disabled&#x22;,
  },
]"
/>

## API reference [#api-reference]

<PropsTable name="AccordionProps" />

`multiple` defaults to `false`. `value` without `onValueChange` fixes the open
sections as given. An empty `items` array renders nothing and warns in
development, as does a duplicated `value`.

## Related [#related]

* [Tabs](./tabs.mdx) swap the whole panel for another, where an accordion reveals
  detail in place.
* [Card](./card.mdx) holds content that stays open, with no header to press.
* [Callout](./callout.mdx) sets apart what a reader cannot miss. A warning never
  goes in a collapsed panel.
