---
title: Accordion
description: details, stacked - every behaviour ships in the element, and items open independently on purpose.
source: https://adamjurek.com/components/accordion
---

## Home


Accordion renders a list of `<details>` elements, one per item, each opening
independently of the others. Reach for it when a page has several expandable
panels - an FAQ, a settings group - and more than one might need to stay open
at once. Reach for Collapsible instead for a single expandable line in prose.

<!-- ::start:showcase demo="accordion" height="380" -->
<!-- ::end:showcase -->

## Why it is built this way

Every behaviour - toggle, keyboard, screen-reader announcement, find-in-page
opening the right panel - ships in `<details>` itself, which is why this
component is markup and a chevron and nothing else. Items open independently
on purpose: an accordion that closes its neighbours when one opens is a radio
group wearing a disclosure costume, and it hides content a reader already
chose to see.

## What it does not do

It takes no callback for open state and no controlled `open` prop past the
first render. `defaultOpen` seeds which ids start open; after that, each
`<details>` runs itself. An accordion that needs to know what is open, or
force an item shut from outside, needs different plumbing than this one.

> [!NOTE] Install commands are not written here
> Anything in `packages/ui/registry.ts` gets its TanStack and shadcn commands
> attached automatically, so there is nothing to keep in sync.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/accordion.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/accordion.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | layout · Disclosure |
| Files | `accordion.tsx` |
| Dependencies | None |
| Also installs | `icon` |
| Tags | details, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { Accordion } from "@sushindustries/ui";

const items = [
	{ id: "shipping", title: "Shipping", content: "Two to four days, tracked." },
	{ id: "returns", title: "Returns", content: "Thirty days, no questions." },
];

export function Example() {
	return <Accordion items={items} defaultOpen={["shipping"]} />;
}
```

## What you should see

Two rows stacked in a bordered box, each with a title and a chevron on the
right. "Shipping" starts open with its content visible underneath;
"Returns" starts closed. Click a summary line and only that row toggles -
the other stays exactly as it was, because each row is its own `<details>`.

## If nothing happens

Nothing toggling almost always means the click landed outside the
`<summary>` line rather than on it - the row's padding is part of the
summary, but the space between rows is not. If a row that should start
open does not, check the id in `defaultOpen` matches an item's `id`
exactly; a typo there fails silently.


## Guides


## When not to use it

Every row here opens independently - opening one never closes another,
and there is no prop to change that. If the actual requirement is "only
one section open at a time", this is the wrong component: build it from
individual `Collapsible` instances and lift the open id into state,
rather than asking this one to fake exclusivity it was not built for.

## Reduced motion

Only the chevron animates - a 180ms rotation when a row opens. Under
`prefers-reduced-motion: reduce` the rotation is instant; the disclosure
itself has no transition to remove, since `<details>` opens and closes
without one on its own.


## API


<!-- generated:api -->

## Props

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `items` | `readonly AccordionItem[]` | - | Rendered in order and keyed by `id`. Each opens without closing the others. |
| `defaultOpen?` | `readonly string[]` | `[]` | Ids open on first render. |

<!-- /generated:api -->

## Notes

`defaultOpen` only affects the first render - there is no controlled
mode, so an id added to the array after mount does not force that row
open; each `<details>` owns its own open state from then on. An id in
`defaultOpen` with no matching item in `items` is silently ignored.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="accordion" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

```tsx
import { Accordion } from "@sushindustries/ui";
import { faqItems } from "./faq.catalogue";

export function FaqSection() {
	return (
		<section className="container section">
			<h2>Questions</h2>
			<Accordion items={faqItems} />
		</section>
	);
}
```

## What this example is not

`faqItems` is built-time content in this example, but `Accordion` has no
opinion about where `items` comes from - a search result, a CMS response,
a hand-written array all work the same way, as long as each entry has a
stable `id`.
