Accordion
A vertically stacked set of interactive headings that each reveal a section of content.
Accordion is a stack of expandable sections. With type="single" (the default) opening a section closes the one that was open, and the open section can be closed again. With type="multiple" the sections open and close independently. defaultValue names the item that is open on load.
Shorthand: <Accordion items={[{ value, trigger, content }]} /> builds the items for you.
Each trigger is rendered inside a heading, <h3> by default. Set headingLevel (2 to 6) so the headings continue the outline of the page: one level below the heading of the section that contains the accordion.
Installation
$ bext ui add accordion
Copy and paste the following into src/components/ui/accordion.tsx.
This component is interactive. Make sure the shared island is loaded (see Islands). It reacts to data-ui="accordion" hooks.
Usage
Anatomy
<Accordion defaultValue="q1"> ├── <AccordionItem value="q1"> │ ├── <AccordionTrigger>Question 1</AccordionTrigger> // <h3><button> │ └── <AccordionContent>Answer 1</AccordionContent> └── <AccordionItem value="q2">...</AccordionItem>
API Reference
Accordion
| Prop | Type | Default | Description |
|---|---|---|---|
| type | "single" | "multiple" | "single" | "single": at most one item is open, and opening one closes the other. "multiple": items are independent. Written to data-type and enforced by the island. |
| defaultValue | string | — | value of the item that is open on load. The shorthand renders it open in the HTML; in the composed form the island opens it, unless you pass open to its trigger and content. |
| items | { value: string; trigger: ReactNode; content: ReactNode }[] | — | Shorthand: generates one AccordionItem per entry. When set, children are ignored. |
| headingLevel | 2 | 3 | 4 | 5 | 6 | 3 | Shorthand only: level of the heading around each trigger. In the composed form, set it on AccordionTrigger. |
| id | string | — | id of the root. In the shorthand it also prefixes the ids that link each trigger to its panel. |
| className | string | — | Extra classes on the root (w-full). |
AccordionItem
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | Identifier of the item, matched against Accordion.defaultValue. |
| children | ReactNode | — | An AccordionTrigger and an AccordionContent. |
AccordionTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
| headingLevel | 2 | 3 | 4 | 5 | 6 | 3 | Level of the heading that wraps the button: <h2> to <h6>. The heading has no margin and uses the font of the trigger, so the level does not change the look. |
| children | ReactNode | — | Label of the <button>. A chevron is appended and rotates 180° when open. |
| open | boolean | false | Renders the trigger in the open state (data-state, aria-expanded, chevron). Pass it together with open on the content. |
| id | string | — | id of the button. Generated by the island when omitted. |
| controls | string | — | id of the panel, written to aria-controls. Set by the island when omitted. |
AccordionContent
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | Panel content. Rendered hidden until the trigger is activated. |
| open | boolean | false | Renders the panel visible in the HTML. |
| id | string | — | id of the panel. Generated by the island when omitted. |
| labelledby | string | — | id of the trigger, written to aria-labelledby. Set by the island when omitted. |
Data Attributes
Attributes you can target in custom CSS to override the style for each state.
| Attribute | On | Values | Description |
|---|---|---|---|
| data-ui="accordion" | Accordion root | — | Root hook. Also carries data-type and data-default-value. |
| data-ui="accordion-item" | AccordionItem | data-value="…" | One section. The island also writes data-state on it. |
| data-ui-accordion-header | heading around AccordionTrigger | — | Marks the <h2> to <h6> that wraps the button. |
| data-state | AccordionTrigger, AccordionItem | "open" | "closed" | Current state of the section, updated by the island. |
| aria-expanded | AccordionTrigger | "true" | "false" | Same state, for assistive technologies. |
| data-ui="accordion-content" | AccordionContent | hidden attribute present or absent | The island shows the panel by removing hidden. |
Keyboard
| Key | Action |
|---|---|
| Space / Enter | Opens or closes the focused section. |
| Tab / Shift+Tab | Moves between the triggers and the focusable content of open panels. |
| Arrow Down / Arrow Up | Moves focus to the next or previous trigger. Wraps at both ends. |
| Home / End | Moves focus to the first or last trigger. |
Accessibility
Each trigger is a native <button> inside a heading, as in the WAI-ARIA accordion pattern, so screen reader users can list the sections and jump between them with heading navigation. The button has aria-expanded, kept in sync by the island, and aria-controls pointing at its panel. Each panel has role="region" and is named by its trigger with aria-labelledby. The ids are written in the HTML by the shorthand when the root has an id, and generated by the island otherwise. A closed panel carries the hidden attribute, so its content is out of the accessibility tree and out of the tab order.
The heading is an <h3> unless headingLevel says otherwise. Pick the level that follows the heading above the accordion, without skipping one: headingLevel={2} directly under the page title, headingLevel={4} inside a section titled with an <h3>. The demos on this page do that.
Examples
Multiple
Heading level
Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.
On This Page