Navigation
Accordion
Disclosure rows that open in place.
Usage
When to reach for it
import { Accordion } from '@misoto22/design'Notes
Radix root, re-exported. Pass type="single" collapsible for an FAQ and type="multiple" for a settings stack; Radix's own discriminated union then types value correctly for each.
Anatomy
| Element | Description |
|---|---|
| Rowrequired | AccordionItem — one hairline-ruled record, keyed by the value Radix opens and closes it by. The set is the root above it: type="single" with collapsible for an FAQ, type="multiple" for a stack of settings. |
| Headingrequired | Radix’s Accordion.Header, which is an <h3> and takes no level prop. Every row therefore adds an h3 to the document outline, wherever the accordion happens to sit. |
| Triggerrequired | The full-width button inside that heading: title against the start edge, marker against the end, py-4. It is also what names the open panel. |
| Markerrequired | A 16px plus, aria-hidden, rotating 45° into a minus when the row opens. The state it draws is carried for everyone else by aria-expanded on the trigger. |
| Panel | Radix’s Content — a role="region" labelled by its trigger, mounted only while open, overflow-hidden so its measured height can animate, with pb-4 pe-8 inside so the text stops short of the marker’s column. It carries data-m22-animated, so the open and close are dropped outright for a reader who asked for less motion. |
Best practices
Do
- Pass collapsible alongside type="single": without it there is no empty value to return to, so the first row the reader opens is a row they can never close again.
- Key each item by something stable rather than by its position — Radix tracks the open row by value, so re-ordering or filtering the list leaves whatever now sits in that slot standing open.
- Write the title as the whole question: it is the accessible name of the panel as well as of the trigger, so a row titled “More” opens a region called “More”.
- Reach for type="multiple" when two rows have to be read against each other — single closes the one the reader was holding in order to open the one they wanted to compare it with.
Don’t
- A closed row’s content is not in the DOM, so an FAQ built out of these is invisible to find-in-page and prints as a list of questions — anything that has to be searchable or printable belongs in the page.
- The trigger is fixed at h3 by Radix’s header, so an accordion under an <h3> lists its rows as that heading’s siblings and the outline goes flat exactly where it should have nested.
- The panel is overflow-hidden — that is what lets the open height animate — so anything inside that must escape the row’s box has to portal out of it; a menu that renders in place is cut off at the row’s edge.
Examples
default
An FAQ: type single with collapsible, so the row the reader opened can be closed again. Without collapsible there is no empty value to return to, and the first row they open is a row they can never close. The marker is a plus rather than a chevron, deliberately — a plus says this opens, a chevron says there is more below, and in a stack of rows that decides whether the reader expects expansion or navigation. Key each row by something stable rather than by its position: Radix tracks the open one by value, so filtering or re-ordering the list leaves whatever now sits in that slot standing open.
a settings stack
type multiple, because these two rows have to be read against each other — single closes the one the reader was holding in order to open the one they wanted to compare it with. defaultValue opens the group they came for. The panel is overflow-hidden, which is what lets its measured height animate, so anything inside that has to escape the row's box — a select's menu, a popover — must portal out of it or be cut off at the row's edge.
How many times a failed job is tried again.
the whole question
title is the accessible name of the panel as well as of the trigger, so a row called More opens a region called More — and a reader moving between regions gets a list of them that names nothing. Write the whole question. The rows on the right say what is behind them before they are opened, which is also what makes the closed stack readable: a column of one-word triggers is a table of contents for a book with no chapter titles.
Opens a region called More
Opens a region called by its question
Parts
Composed at the call site rather than configured through props, so a layout this component did not anticipate is still expressible.
AccordionItem
One disclosure row: a hairline-ruled trigger and its panel.
The marker is a plus that rotates into a minus, not a chevron. A chevron says "there is more below"; a plus says "this opens" — and in a stack of rows the difference decides whether the reader expects navigation or expansion.
The panel animates on Radix's own --radix-accordion-content-height, so it opens to its real height without measuring anything at the call site.
| Prop | Type | Default | Description |
|---|---|---|---|
| childrenrequired | ReactNode | — | |
| titlerequired | ReactNode | The row's summary — what the reader clicks. Named `title` rather than inherited from the DOM attribute of the same name, which is a tooltip. |
Also accepts everything in Omit<ComponentProps<typeof AccordionPrimitive.Item>, 'children' | 'title'>. Those are forwarded to the underlying element and are not listed row by row.
Re-exports
Accordion = AccordionPrimitive.RootRadix root, re-exported. Pass type="single" collapsible for an FAQ and type="multiple" for a settings stack; Radix's own discriminated union then types value correctly for each.
Keyboard
| Key | Does |
|---|---|
| Tab | Moves between rows. |
| EnterSpace | Opens or closes the focused row. |