Accordion
A list of items that each open in place: a trigger row over a panel whose height follows its content.
import { Accordion } from "@butteryui/react";Demo
When to use it
Use it for
- Questions and answers, or any list of titles whose details only some people need
- Long details or settings in parts that people open one or a few at a time, such as an account's
- A narrow panel with more in it than fits, where each part's title is enough to pick it
Not for
- A whole section of questions with groups and a search: use Faq
- Switching between views of one thing: use Tabs
- One part that shows more: a button over a Morph
- What everyone needs to read: show it instead of folding it away
Examples
Account sections
A customer's account in parts that open several at once, each with the controls that change it.
Your next bag roasts on Thursday morning.
Brew step by step
A pour-over guide whose page keeps the open step, so Next step opens the one after it.
Then: Let it bloom
Four looks
The same questions as divided rows, rows in a card, a card each or plain rows, at each size.
Pickup paused
Ways to get an order, one of them paused: a disabled item stays shut and the keyboard passes over it.
Shipping questions
The shop's shipping questions, one open at a time.
Variants and states
Divided
Rows with a hairline between them, lined up with the text around them. The default.
Contained
Rows in one card, divided by hairlines, tinting on hover.
Cards
A card for each item, 8px apart.
Plain
Rows without lines that tint on hover, for a sidebar or a panel.
Sizes
sm for a panel, md (the default) for an app page, lg for a page's own questions.
size="sm"
size="md"
size="lg"
Icons, avatars and descriptions
An icon sits on the title's first line; anything else in leading centers on the title and its description.
Disabled
A disabled item can't open and leaves the tab order; the arrow keys skip it.
Props
Accordion
| Prop | Type | Default | Description |
|---|---|---|---|
type | "single" | "multiple" | "single" | "single" (the default) keeps one item open at a time; opening another closes it. "multiple" lets any number of items stay open at once. |
value | string | readonly string[] | With type="single": The open item's value, or "" while none is. Pair with onValueChange to control it. With type="multiple": The open items' values. Pair with onValueChange to control them. | |
defaultValue | string | readonly string[] | With type="single": The item open at first when uncontrolled. Defaults to none. With type="multiple": The items open at first when uncontrolled. Defaults to none. | |
onValueChange | ((value: string) => void) | ((value: readonly string[]) => void) | With type="single": Called with the value of the item that opens, or "" when the open one closes. With type="multiple": Called with the open items' values each time one opens or closes. | |
collapsible | boolean | true | With type="single": Whether pressing the open item's trigger closes it. Defaults to true. |
variant | "divided" | "contained" | "cards" | "plain" | "divided" | How the items are drawn: "divided" (the default), rows with a hairline between them; "contained", rows
in one card; "cards", a card for each item, 8px apart; or "plain", rows that tint on hover. |
size | "sm" | "md" | "lg" | How big the rows are: "sm" (14px titles, 12px above and below), "md" (the default: 15px titles, 16px
above and below) or "lg" (18px titles, 20px above and below). | |
disabled | boolean | Keeps every item from opening or closing. | |
headingLevel | 2 | 3 | 4 | 5 | 6 | 3 | The heading level each trigger sits in, one below the heading over the accordion. Defaults to 3. |
children | ReactNode | The items: Accordion.Items, as direct children. |
It also takes Omit<ComponentPropsWithRef<"div">, "defaultValue" | "dir">.
Accordion.Item
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | string | Names the item: what the accordion's value holds while it's open. | |
titlerequired | ReactNode | The trigger's words, such as a question. They wrap onto more lines, and they name the trigger and its panel. | |
description | ReactNode | A line under the title, such as where the item comes from. Screen readers hear it after the title, as part of the trigger's name. | |
icon | ComponentType<IconComponentProps> | An icon before the title, on the middle of its first line. | |
leading | ReactNode | Anything else before the title, such as an Avatar, centered on the title and description. It takes the place of icon. | |
disabled | boolean | Keeps the item from opening or closing, and takes its trigger out of the tab order. | |
children | ReactNode | The panel: what shows under the trigger while the item is open. |
It also takes Omit<ComponentPropsWithRef<"div">, "title">.
Accessibility
Each trigger is a button in a heading (an h3 unless headingLevel says otherwise), named by its title followed by its description, with aria-expanded, and aria-controls naming its panel while it's open; the panel is a region named by the title alone. Tab moves from trigger to trigger and into an open panel; the arrow keys move between triggers, wrapping at the ends, and Home and End reach the first and last. A disabled item's trigger is left out of both.
| Keys | What they do |
|---|---|
| Enter or Space | Opens or closes the item whose trigger has focus |
| Tab | Moves to the next trigger, going through an open panel's content first |
| Arrow Down or Arrow Up | Moves to the next or previous trigger, wrapping at the ends and skipping disabled ones |
| Home or End | Moves to the first or last trigger |
Motion
It changes shape as one surface, on `spring.smooth`, or `spring.morph` for a big change, rather than closing and reopening.
With reduced motion: The panel opens and closes at once and the chevron turns over at once; the panel's content fades in and out in 100ms.
Layout
It fills its container's width with its items stacked: touching, with a hairline between them (divided); in one card (contained); 8px apart, a card each (cards); or 2px apart (plain). A trigger row is as tall as its title, which wraps onto more lines and never truncates, with 12, 16 or 20px above and below it by size; the whole row is the trigger's hit area. Its chevron and an icon sit on the middle of the title's first line, and the panel's content starts at the row's padding edge, under the icon. Divided rows have no side padding, so their words line up with the text around them.