A stack of headed sections, each opening and closing its own content under its heading, such as the questions on a help panel or the details of each planned work on a line.
When to use
Use Accordion for a stack of headed sections a person reads one or two of, each opening under its heading, such as the questions on a help panel or the details of each planned work on a line.
Kongens Nytorv
Metro M1, M2 and M3.
Props
| Name | Type | Default | Description |
|---|---|---|---|
value | string[] | — | The values of the open items, when the caller controls them. [] opens none. |
defaultValue | string[] | [] | The values of the items open at first, when Accordion keeps them itself. Left out, every item starts closed. |
onChange | (event: Event, value: string[], details: Accordion.Root.ChangeEventDetails) => void | — | Called when an item opens or closes, with the values of the items open after it: by a press, Enter or Space on a trigger (details.reason 'trigger-press'), or by the browser's find in page with hiddenUntilFound ('none'). event is details.event. |
children | ReactNode | — | The AccordionItems, in reading order. |
className | string | — | Reaches the root. className is merged after pn-Accordion. |
style | CSSProperties | — | Reaches the root. className is merged after pn-Accordion. |
ref | Ref<HTMLDivElement> | — | Reaches the root. className is merged after pn-Accordion. |
valuestring[], default —- The
values of the open items, when the caller controls them.[]opens none. defaultValuestring[], default[]- The
values of the items open at first, when Accordion keeps them itself. Left out, every item starts closed. onChange(event: Event, value: string[], details: Accordion.Root.ChangeEventDetails) => void, default —- Called when an item opens or closes, with the
values of the items open after it: by a press, Enter or Space on a trigger (details.reason'trigger-press'), or by the browser's find in page withhiddenUntilFound('none').eventisdetails.event. childrenReactNode, default —- The
AccordionItems, in reading order. classNamestring, default —- Reaches the root.
classNameis merged afterpn-Accordion. styleCSSProperties, default —- Reaches the root.
classNameis merged afterpn-Accordion. refRef<HTMLDivElement>, default —- Reaches the root.
classNameis merged afterpn-Accordion.
Use it
import { Accordion } from '@mylder/perron'Ask your agent
get_reference({ library: "base-ui", component: "Accordion" })Keyboard and ARIA
From the Base UI reference for Accordion and the installed AccordionRoot, AccordionItem, AccordionTrigger and AccordionPanel. "From" says who does it.
| Key | Does | From |
|---|---|---|
Tab | Moves focus to the next trigger, or into an open panel's first focusable element, in document order; skips a disabled trigger | The browser |
Shift+Tab | Moves focus back the same way | The browser |
Enter, Space | Open the focused trigger's panel, or close it while open; with multiple off, opening one closes the other | The browser's click; Base UI for a non-button render |
The arrow keys, Home and End do nothing: Base UI 1.9.0 follows the APG's update to the accordion pattern, which drops the roving focus, and the component adds no keys of its own.
| Element | Role | Accessible name from | States announced |
|---|---|---|---|
root | none: a <div> | none | none |
item | none: a <div> | none | none |
header | heading, level 3, or the level slotProps.header.render gives | Its text, the trigger's | none |
trigger | button, from the native <button> | Its text content: the title; the chevron is aria-hidden | aria-expanded; aria-controls while open; disabled by the native disabled |
chevron | none: hidden from screen readers | none | none |
panel | region, from Base UI | Its trigger, through aria-labelledby, which Base UI sets | none |
What axe won't catch, from the contract, each with how this component meets it:
- Everything the eye is told, the ear is told, for every shape each prop accepts: whether a panel is open, which the chevron shows, is the trigger's
aria-expanded; a disabled trigger's tertiary colour is the nativedisabled; the chevron itself isaria-hidden. - Every landmark and named region has an accessible name: each open panel is a
regionnamed by its trigger through Base UI'saria-labelledby, so its name is the item'stitle, which is required. - Every default label can be overridden: none here; Accordion owns no strings.
- Every shape the type accepts renders, in every variant:
titlerenders a string, an element and a list alike, as phrasing content in the button;childrenrenders a string, an element and a list alike in the panel's<div>. - A hidden copy kept for animation is
aria-hidden, and a collapsed region isinert: a closed panel is unmounted, or withkeepMountedorhiddenUntilFoundkepthidden, which takes it out of the accessibility tree and the Tab order asinertwould; Accordion keeps no copy for animation: a closing panel is the panel itself, shrinking forduration.switchunder a trigger that already saysaria-expanded="false", then unmounted or hidden by Base UI. - Native semantics first: a native
<h3>holding a native<button>, with Base UI'saria-expanded,aria-controlsand the panel'sregionnamed byaria-labelledby; no ARIA from the caller. - Focus moves in reading order and returns on close: Tab goes from a trigger into its open panel and on to the next trigger, in document order; closing a panel leaves focus on the trigger that closed it.
- Every target is at least
size.target-minsquare: the smallest target is a trigger with a one-word title,size.button(36 px) tall and the accordion's full width. - Loops stop when the system asks for less motion: no loop; the panel's height and the chevron's turn are transitions over
duration.switch, which the tokens shorten to 0.01 ms under reduced motion, so both change at once with no query in the component (CR-10).
Owned strings
No owned strings: every word comes from the caller, as an item's title and its children.
Do and don't
For a caller using the component, with the reason. Each row is a rule: a must row's Don't is never right, a should row's needs a reason, and Checked by says what catches the Don't in a caller's code.
| Rule | Level | Do | Don't | Checked by |
|---|---|---|---|---|
| DD-1 | should | Use Accordion for sections a person reads one or two of, such as help questions or the details of each planned work | Hide what most people need behind a heading, such as a stop's next departures: show it, under a Section | review |
| DD-2 | should | Use Tabs to switch between views of one thing | Use Accordion as tabs stacked down the page: an accordion's sections are read in place, one under another | review |
| DD-3 | must | Write each title as a few words or a short question in sentence case, saying what the panel holds: "What the dot shapes mean" | Write "Read more" or "Details" on every item: each trigger and its region are named by the title, so every one would sound the same | review |
| DD-4 | must | Keep a title to words: an icon or a badge beside it is fine, a link or a button is not | Put a link or a button in a title: it renders inside the trigger's <button>, where nothing else can be pressed | review |
| DD-5 | must | Give items a value when the accordion opens one at first or the caller controls it | Read a generated value from onChange and store it: it changes from one mount to the next | review |
| DD-6 | should | Set multiple when people compare items side by side | Leave items that are read together closing each other | review |
| DD-7 | must | Set the heading level with slotProps.header.render (render: <h4 />) when the accordion sits under an h3; under a panel's or a dialog's h2, keep the h3 | Leave the outline skipping a level, or wrap the title in a heading of your own inside the button | review |
| DD-8 | should | Use hiddenUntilFound on long help pages, so the browser's find in page reaches closed answers | Use keepMounted to make closed content searchable: a hidden panel is out of find in page as well | review |
| DD-9 | must | Leave out an item whose panel would have nothing in it, or open it to an EmptyState that says why | Disable an item because its panel has nothing to show: a disabled trigger can't say why, and it leaves the Tab order | review |