Skip to content

Accordion

  • A FAQ, where each question opens its answer
  • Long settings or order details split into sections people open as needed
  • Content that should be findable with the browser’s find-in-page while closed

Not when people need to compare sections side by side, or every section is short — show them open instead.

Choosing between organising content:

ComponentUse it forFor example
AccordionSections stacked on one page, of which people read one or a fewFAQ, settings groups, order details
TabsViews of the same thing at the same level, one at a timeAccount / Billing / Security
CardA self-contained group of content, shown as one unit, often in a grid or listA product tile, a project summary
TableRows of records compared across the same columnsInvoices, users, order lines
SeparatorA line that divides sections, with no content of its ownBetween menu groups or toolbar groups
How long does shipping take?
Two to five working days, tracked.
Can I return an order?
Within thirty days, unused, with the receipt.
Do you ship abroad?
To most of Europe and North America.
import { Accordion } from '@arun-dev/ui';
const faq = [
{ q: 'How long does shipping take?', a: 'Two to five working days, tracked.' },
{ q: 'Can I return an order?', a: 'Within thirty days, unused, with the receipt.' },
{ q: 'Do you ship abroad?', a: 'To most of Europe and North America.' },
];
// exclusive: opening one closes the others — the browser does it, through <details name>.
export default function AccordionBasics() {
return (
<Accordion.Root exclusive style={{ inlineSize: '28rem', maxInlineSize: '100%' }}>
{faq.map(({ q, a }, index) => (
<Accordion.Item key={q} defaultOpen={index === 0}>
<Accordion.Trigger>{q}</Accordion.Trigger>
<Accordion.Panel>{a}</Accordion.Panel>
</Accordion.Item>
))}
</Accordion.Root>
);
}

Accordion is built from native <details> and <summary>. The browser does the work:

  • A press, Enter or Space toggles an item.
  • Items with the same name open one at a time. exclusive on the Root gives them one.
  • In Chromium, find-in-page opens a closed item when the match is inside it.
  • Opening animates where the browser supports it (::details-content), and snaps elsewhere and under reduced motion.
import { Accordion } from '@arun-dev/ui';
<Accordion.Root exclusive>
<Accordion.Item defaultOpen>
<Accordion.Trigger>Shipping</Accordion.Trigger>
<Accordion.Panel>Two to five working days.</Accordion.Panel>
</Accordion.Item>
</Accordion.Root>;
PartRendersDoes
Root<div>optional; exclusive makes its Items open one at a time
Item<details>one section; works alone as a single disclosure
Trigger<summary>the always-visible line, with a chevron
Panel<div>what the Item reveals

defaultOpen sets the starting state. To control it, pass open and update it from onToggle:

<Accordion.Item open={open} onToggle={(event) => setOpen(event.newState === 'open')}>
Shipping
Everything about shipping.
Returns
Everything about returns.
Warranty
Everything about warranty.

Open two items: with exclusive the first closes as the second opens; without it, both stay open.

<Accordion.Root exclusive><Accordion.Item /> …</Accordion.Root>
Accordion.Root props
PropTypeDefaultDescription
exclusivebooleanfalseOnly one Item open at a time: every Item gets the same generated name.
classNamestringConcatenated with the component's own classes, never replacing them.
renderReactElementElement or component to render instead of the default. Props, className, event handlers and ref are merged onto it.
refRef<HTMLElement>Ref to the rendered element. Merged with any ref on the render element.
…restHTMLAttributesAnything else is spread onto the rendered element, so id, aria-*, data-* and event handlers all reach the DOM.
Accordion.Item props
PropTypeDefaultDescription
defaultOpenbooleanfalseOpen at mount. Read once; the browser owns it after that.
openbooleanControlled state. Pair with onToggle, whose event has newState: "open" or "closed".
namestringItems with the same name open one at a time, natively. Overrides the Root's.
  • <summary> is a button to screen readers, so a heading inside it isn’t reached by heading navigation. That’s the price of the native element. If the sections need headings, put one above the accordion.
  • Restyle with the --accordion-* tokens.