Accordion
A set of disclosures, each one a <details>. When the root is type="single",
the items share a name and the browser closes the open one — exclusivity with
no state, no registry and no effect.
import { Accordion } from '@apostel/bedrock'
Source
import { Accordion } from '../../src/index'
/**
* Each item is a `<details>`, and `type="single"` gives the siblings a shared
* `name` — so the browser closes the open one. Exclusivity survives with
* JavaScript disabled, and cannot drift out of step with React state.
*/
export default function AccordionDemo() {
return (
<Accordion.Root type="single" style={{ width: '100%', maxWidth: '28rem' }}>
<Accordion.Item value="shipping">
<Accordion.Header>
<Accordion.Trigger>When does it ship?</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>Within two working days, tracked.</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="returns">
<Accordion.Header>
<Accordion.Trigger>Can I return it?</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>Thirty days, unopened, no questions.</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="support">
<Accordion.Header>
<Accordion.Trigger>How do I get support?</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>Reply to your order email and it reaches a person.</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
)
}Anatomy
<Accordion.Root>
<Accordion.Item>
<Accordion.Header>
<Accordion.Trigger />
</Accordion.Header>
<Accordion.Content />
</Accordion.Item>
</Accordion.Root>
Accordion.Root
Renders a <div>. All the behaviour is in the items.
| prop | type | notes |
|---|---|---|
type |
'single' | 'multiple' |
Default single. Sets whether items share a name. |
defaultValue |
string | string[] |
Which items start open. |
orientation |
'horizontal' | 'vertical' |
Default vertical. |
onValueChange |
(value: string[]) => void |
Reports which items are open after a change. |
asChild |
boolean |
Supported. |
type="single" always allows closing the open item, because a <summary>
toggles. Radix's collapsible={false} has no native equivalent and is not
offered — see migration.
There is no value prop here. For a controlled accordion, import from
@apostel/bedrock/controlled.
Accordion.Item
Renders <details data-bedrock-accordion-item>, with name shared across
siblings when the root is single.
| prop | type | notes |
|---|---|---|
value |
string |
Required. Identifies the item, and is written as data-value. |
disabled |
boolean |
Removes the summary from the tab order and blocks the toggle. |
asChild |
boolean |
The child must render a <details>. |
open is read once, on the first render. React only writes a DOM property when
the value it renders changes, so the user's own toggling is never undone by an
unrelated re-render.
A disabled disclosure is not a native concept. data-disabled is the closest
thing: style it, and the summary is not focusable.
Accordion.Header
Renders <summary>.
Radix renders <h3><button aria-expanded>. The platform's disclosure pattern is
a summary, which carries the expanded state itself. If you want the heading
semantics the APG asks for, put a heading inside this:
<Accordion.Header>
<h3>Shipping</h3>
</Accordion.Header>
That is valid HTML and keeps the summary's behaviour.
Accordion.Trigger
Renders a <span>, and is not itself interactive. The summary already is,
and nesting a button inside it would produce two tab stops for one control.
It exists so Radix-shaped markup keeps its styling hook. If you are writing new
markup, you can leave it out and style Accordion.Header directly.
Accordion.Content
Renders a <div>. Children mount only while the item is open, so a form
inside a closed panel resets itself.
Server-rendered markup is the exception: content renders on the server and on the hydrating render that must match it, so a page whose JavaScript never arrives still has a complete, working accordion.
Keyboard
| key | behaviour |
|---|---|
Enter/Space |
On the summary, toggles. Native. |
Tab |
Moves between summaries and into open content. Native. |
Arrow keys do not move between items. <details> has no such behaviour, and
adding it would mean intercepting keys the browser already assigns. See
gaps.
What is not here
- Animated height.
<details>has no height animation in every engine yet. See styling for what does work today. collapsible={false}. No native equivalent, as above.