Components / disclosure
Accordion
Radix is permitted here — Accordion is on the closed list of six.
Summary
A stack of disclosures at one heading level — sections of a long form, a FAQ, an inspector.
| APG pattern | Accordion |
| Built on | Radix |
| Status | stable |
| Since | 0.1.0 |
<Accordion headingLevel={3} type="single" items={[{ id: 'a', summary: 'Intro', content: … }]} />
Anatomy
Each trigger sits inside a real heading element, which is how a screen-reader user skims the set.
| Part | Element | Required |
|---|---|---|
content | <radixaccordion.content> | yes |
description | <span> | no |
heading | <heading> | yes |
indicator | <svg> | yes |
item | <radixaccordion.item> | yes |
label | <span> | yes |
panel | <div> | yes |
trigger | <radixaccordion.trigger> | yes |
Properties
| Property | Values | Default | What it does |
|---|---|---|---|
items | AccordionItem[] | required | The sections, in display order. Each needs a stable id — it is what value refers to. |
type | single · multiple | single | single shows one panel at a time; multiple allows any number. |
collapsible | boolean | true | Only with type="single" — lets the open panel be closed again. |
headingLevel | 1 | 2 | 3 | 4 | 5 | 6 | 3 | The heading level wrapping each trigger. Pick the one that fits the page's outline; a wrong level breaks heading navigation for screen-reader users. |
value | string | string[] | none | The open section, or sections when type="multiple". Passing it makes the accordion controlled. |
defaultValue | string | string[] | none | Which section starts open when the accordion keeps its own state. |
onValueChange | (value: string | string[]) => void | none | Called with the new open section, or the array of them under type="multiple". |
States
| State | Expressed as | |
|---|---|---|
| Focused | :focus-visible | |
| Hover | :hover | |
| Open or closed | data-state |
Behavior
single closes the open panel when another opens. multiple leaves them.
Accessibility
Set headingLevel deliberately. The right level depends on what surrounds the accordion, so the component will not guess it — a wrong level breaks the document outline, which is the thing a screen-reader user navigates by.
aria-expanded and aria-controls on each trigger, from Radix.
The heading carries no font of its own — it inherits, because the trigger inside it is the styled thing. A heading that looks like a heading inside an accordion competes with the page's real ones.
Keyboard
| Key | Result |
|---|---|
Enter / Space | Toggles the panel |
Arrow Up / Arrow Down | Move between triggers |
Home / End | Jump to the first or last trigger |
Roles, states and properties
| Attribute | Set by |
|---|---|
aria-controls | set at render |
aria-describedby | set at render |
aria-hidden | always true |
aria-labelledby | set at render |
Focus
Each trigger is a tab stop; the panel's contents follow in document order. Radix supplies the arrow behavior.
Screen reader
Not recorded as verified. Stage 4 of the readiness ladder — driving each component with VoiceOver and NVDA — is unstarted across the package, and no component should claim otherwise until docs/journal/ records the session that did it.
Contrast and target size
npm run check:contrast audits every color pair this component uses across all four skins, light and dark, and fails the build on a shortfall. Two waivers are on the record in docs/audit/DEFERRED.md.
It sets no target size of its own; anything interactive inside it brings its own.
Tested
test/apg/partials-sweep.test.jsxtest/browser/upstream-natives.spec.mjs
Known gaps
_No upstream requirement is recorded as not applicable._
Appearance
| Token | Used for |
|---|---|
--se-accent | color |
--se-border | border-block-end, border-block-start |
--se-border-width | border-block-end, border-block-start |
--se-control-h | min-block-size |
--se-duration-fast | animation, transition |
--se-duration-instant | transition |
--se-duration-medium | animation |
--se-ease-standard | animation, transition |
--se-focus-color | outline |
--se-focus-width | outline, outline-offset |
--se-icon-sm | block-size, inline-size |
--se-leading-body | line-height |
--se-radius-sm | border-radius |
--se-space-1 | row-gap |
--se-space-3 | column-gap, padding-block |
--se-space-4 | padding-block-end |
--se-text | color |
--se-text-muted | color |
--se-text-sm | font-size |
--se-text-xs | font-size |
--se-weight-regular | font-weight |
Motion
Height animates over --se-duration-fast.
Content
Each summary names its section as a label, not a question — unless the accordion is a FAQ, where a question is exactly right.
Usage
type="single" is the default and is usually right. Use multiple only when readers genuinely need two panels open to compare them; otherwise it lets the page grow without limit.
An accordion is a set; a disclosure is one thing. The difference is not size. Every trigger here sits inside a heading at one shared level, so a screen-reader user can skim the set by heading, and type="single" can enforce that only one panel is open at a time. A Collapsible has none of that — it knows about no other collapsible on the page — so stacking three of them produces something that looks like this and behaves like three unrelated toggles.
When not to use it. For one section, use Collapsible. If the sections are alternatives and only one is ever relevant, use Tabs.
Notes
Radix is permitted here — Accordion is on the closed list of six.