bx

Accordion

A vertically stacked set of interactive headings that each reveal a section of content.

tsx
<div className="w-full max-w-md">
  <Accordion>
    <AccordionItem value="1">
      <AccordionTrigger headingLevel={2}>Is it keyboard operable?</AccordionTrigger>
      <AccordionContent>Yes. Every trigger is a native button: Tab reaches it, Space or Enter toggles it.</AccordionContent>
    </AccordionItem>
    <AccordionItem value="2">
      <AccordionTrigger headingLevel={2}>Is it styled?</AccordionTrigger>
      <AccordionContent>Yes. It comes with default styles that match the rest of shadcn/ui.</AccordionContent>
    </AccordionItem>
    <AccordionItem value="3">
      <AccordionTrigger headingLevel={2}>Is it animated?</AccordionTrigger>
      <AccordionContent>Yes. It's animated by default.</AccordionContent>
    </AccordionItem>
  </Accordion>
</div>

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

Terminal
$ bext ui add accordion

Usage

tsx
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@bext-stack/ui/primitives"

<Accordion defaultValue="item-1">
  <AccordionItem value="item-1">
    <AccordionTrigger>Is it styled?</AccordionTrigger>
    <AccordionContent>Yes. It comes with default styles.</AccordionContent>
  </AccordionItem>
</Accordion>

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

Orders placed before noon leave the same day.

tsx
<Accordion
  type="multiple"
  id="faq"
  defaultValue="shipping"
  headingLevel={4}
  items={[
    { value: "shipping", trigger: "Shipping", content: "Orders placed before noon leave the same day." },
    { value: "returns", trigger: "Returns", content: "You have 30 days to send an item back." },
    { value: "warranty", trigger: "Warranty", content: "Two years on every product." },
  ]}
/>

Heading level

tsx
{/* This section is titled with an <h3>, so the triggers are <h4>. */}
<Accordion>
  <AccordionItem value="plan">
    <AccordionTrigger headingLevel={4}>Can I change plan later?</AccordionTrigger>
    <AccordionContent>Yes. The change applies from the next invoice.</AccordionContent>
  </AccordionItem>
  <AccordionItem value="cancel">
    <AccordionTrigger headingLevel={4}>How do I cancel?</AccordionTrigger>
    <AccordionContent>From the billing page, at any time.</AccordionContent>
  </AccordionItem>
</Accordion>
Try it live
Open in play

Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.

src/app/page.tsx 2 files · signals · runs in your browser
Pure PRISM + real bext signals: fine-grained, no re-render.