Accordion
Stacked headings that each reveal a section of content — a heading, a button, and a labelled region, exactly as the WAI-ARIA Accordion pattern specifies.
Usage#
import {
Accordion,
AccordionContent,
AccordionHeader,
AccordionItem,
AccordionTrigger,
} from "neelam-ui";
<Accordion defaultValue="item-1">
<AccordionItem value="item-1">
<AccordionHeader>
<AccordionTrigger>Is it accessible?</AccordionTrigger>
</AccordionHeader>
<AccordionContent>Yes.</AccordionContent>
</AccordionItem>
</Accordion>AccordionHeader wrapping AccordionTrigger is not optional decoration — the
pattern is a heading containing a button, which is what puts each section in
the document outline.
Single or multiple#
type="single" (the default) keeps at most one item open, and value is a
string. collapsible lets you close the open one by activating it again;
without it, one item is always open.
type="multiple" allows any number open at once, and value becomes an array:
Heading levels#
AccordionHeader renders an <h3>. Nest the accordion under an appropriate
heading in your own outline so the levels descend without skipping — the same
consideration Card's title has.
Closed content stays mounted#
AccordionContent animates with a grid-template-rows transition from 0fr to
1fr, because height: auto is not a transitionable value. That means closed
content remains in the DOM.
Closed panels are inert, not just clipped
Content hidden by the collapsed row would otherwise still be tabbable and
still exposed to assistive tech. AccordionContent marks itself inert while
closed, so it is genuinely unreachable — the same problem
Sidebar solves when it collapses.
This differs from Tabs, which unmounts inactive
panels outright: tabs are not animated, so there is nothing to keep them around
for. Here, a panel's internal state does survive being closed.
Controlled#
const [open, setOpen] = useState("item-1");
<Accordion value={open} onValueChange={setOpen}>…</Accordion>For type="multiple", value and onValueChange work with string[].
Keyboard#
| Key | Behaviour |
|---|---|
| Tab | Moves to the next trigger, then into an open panel's content. Closed panels are inert and skipped. |
| EnterSpace | Toggles the focused section — native button activation. |
Arrow keys are not bound here
The APG lists arrow-key movement between headers as optional, and each trigger is an ordinary tab stop instead. That keeps browser find-in-page and normal tabbing behaving predictably across a long page of sections.
Accessibility#
- The structure is the pattern:
<h3>→<button aria-expanded aria-controls>→<div role="region" aria-labelledby>. Each panel is named by its own trigger. aria-expandedon the trigger is what announces open or closed; the chevron isaria-hiddendecoration.- Closed panels are
inert, so collapsed content is never a set of invisible tab stops. - The expand transition is dropped entirely under
prefers-reduced-motion(WCAG 2.3.3) — sections snap open instead.
API reference#
Accordion (single)#
| Prop | Type | Default |
|---|---|---|
collapsibleAllows closing the open item by activating it again. Defaults to `false`. | boolean | — |
defaultValue | string | — |
onValueChange | ((value: string | undefined) => void) | — |
typeAt most one item open at a time. This is the default. | "single" | — |
valueControls which item is open. Omit to let the accordion manage its own state. | string | — |
Accordion (multiple)#
| Prop | Type | Default |
|---|---|---|
typerequiredAny number of items can be open at once. | "multiple" | — |
defaultValue | string[] | — |
onValueChange | ((value: string[]) => void) | — |
value | string[] | — |
AccordionItem#
| Prop | Type | Default |
|---|---|---|
valuerequiredIdentifies this item — the value passed around by `Accordion`'s `value`/`onValueChange`. | string | — |