Accordion
Organize related disclosures into an accessible single-open or always-open group, using Collapse as the state and event layer.
Single-open accordion
An accordion is a styled group of Collapse panels. Add data-bs-accordion to the group and connect every native button to one uniquely identified .bs-collapse panel. Initialize no more than one panel as open in the default mode. After initialization, opening an item closes the open sibling, while closing the active item may leave the whole group closed.
<div class="bs-accordion" data-bs-accordion>
<section class="bs-accordion-item">
<h2 class="bs-accordion-header">
<button class="bs-accordion-trigger" id="account-trigger" type="button"
data-bs-toggle="collapse" aria-controls="account-panel"
aria-expanded="true">
Account settings
<i data-lucide="chevron-down" class="bs-accordion-icon bs-icon" aria-hidden="true"></i>
</button>
</h2>
<div class="bs-collapse bs-accordion-panel" id="account-panel"
role="region" aria-labelledby="account-trigger">
<div class="bs-accordion-body">Account settings content.</div>
</div>
</section>
<section class="bs-accordion-item">
<h2 class="bs-accordion-header">
<button class="bs-accordion-trigger" id="billing-trigger" type="button"
data-bs-toggle="collapse" aria-controls="billing-panel"
aria-expanded="false">
Billing and invoices
<i data-lucide="chevron-down" class="bs-accordion-icon bs-icon" aria-hidden="true"></i>
</button>
</h2>
<div class="bs-collapse bs-accordion-panel" id="billing-panel"
role="region" aria-labelledby="billing-trigger" hidden>
<div class="bs-accordion-body">Billing content.</div>
</div>
</section>
</div>
Structure
Keep every trigger inside a heading at the correct level for the page. The panel may contain any semantic flow content.
Initial state
Leave an initially open panel without hidden; add hidden to closed panels. Initialization synchronizes aria-expanded and data-bs-state.
Indicator
.bs-accordion-icon rotates from the trigger's managed aria-expanded state. Mark a decorative icon aria-hidden="true".
Always-open groups
Add data-bs-accordion-always-open when each item should operate independently. Opening an item no longer closes its siblings, so any number of panels can remain open. The example also combines the optional compact and flush visual treatments.
<div class="bs-accordion bs-accordion-compact bs-accordion-flush"
data-bs-accordion data-bs-accordion-always-open>
<!-- Use the same item, heading, trigger, and panel structure. -->
</div>
Visual variants
Modifiers affect presentation only. They can be used separately or together and do not change state, events, or accessibility behavior.
| Class | Effect | Good fit |
|---|---|---|
.bs-accordion | Bordered surface with large radius and standard spacing. | Standalone FAQ and settings groups. |
.bs-accordion-compact | Reduces trigger and body padding. | Dense side panels and administrative screens. |
.bs-accordion-flush | Removes the inline outer borders and corner radius. | Accordions placed edge-to-edge inside another surface. |
Accessibility
- Use a native
buttonfor every trigger. Native buttons provide Enter and Space activation and participate in normal Tab order. - Place each button inside a heading whose level follows the page outline. Do not choose a heading level solely for its visual size.
- Give each panel a unique
idand point the trigger'saria-controlsto it. Give the trigger its ownidwhen the panel usesaria-labelledby. role="region"can make a small set of important panels easier to discover. Omit it when a page contains many panels so the landmark list does not become noisy.- Keep focus on the trigger after a panel changes. Do not move focus into the panel unless a separate user action requires it.
- Boobstrap does not add arrow-key navigation to accordions. This keeps behavior aligned with native disclosure buttons; users move between triggers with Tab and Shift+Tab.
The icon transition follows prefers-reduced-motion: Boobstrap removes that transition when the user requests reduced motion.
Initialization and lifecycle
The aggregate initializer finds every [data-bs-accordion] group and every identified .bs-collapse panel at or below the supplied root. Repeated calls reuse existing instances. Call destroy() on the returned lifecycle handle before removing or replacing an enhanced subtree.
import { initBoobstrap } from "@boobstrap/boobstrap/js";
const settings = document.querySelector("#settings");
const boobstrap = initBoobstrap(settings);
// Before replacing or removing #settings:
boobstrap.destroy();
For direct control, import both controllers. Creating the Accordion instance also reuses or creates Collapse instances for its child panels; Collapse owns the actual open and closed state.
import { Accordion } from "@boobstrap/boobstrap/js/accordion";
import { Collapse } from "@boobstrap/boobstrap/js/collapse";
const root = document.querySelector("#billing-accordion");
const group = Accordion.getOrCreateInstance(root);
const collapses = [...root.querySelectorAll(".bs-collapse[id]")]
.map((panel) => Collapse.getOrCreateInstance(panel));
const invoice = collapses.find((collapse) => collapse.element.id === "invoice-panel");
invoice.show();
// Manual teardown owns both layers.
group.destroy();
for (const collapse of collapses) collapse.destroy();
Collapse.getOrCreateInstance(panel), then recreate the group controller if it must coordinate that item. The Accordion controller captures its panel list when it is constructed.Vanilla events and cancelation
The vanilla Accordion controller does not introduce a second event family. Its items use the bubbling Collapse lifecycle events on each panel. The two before-events are cancelable; the completed events are observational. Alpine exposes onOpenIdsChange instead. React and Vue expose group change callbacks plus events for transitions initiated through each useCollapse result; do not assume that a group-driven sibling update emits the vanilla hide sequence.
| Event | Cancelable | When it fires |
|---|---|---|
bs:collapse:show | Yes | Before a closed panel opens. |
bs:collapse:shown | No | After the panel is visible and public state is synchronized. |
bs:collapse:hide | Yes | Before an open panel closes. |
bs:collapse:hidden | No | After the panel is hidden and public state is synchronized. |
In a single-open group, Accordion handles a panel's bs:collapse:show by asking every open sibling to hide. If any sibling's bs:collapse:hide event is canceled, Accordion also cancels the requested opening. This prevents the group from silently entering a multiple-open state.
const group = document.querySelector("#profile-accordion");
group.addEventListener("bs:collapse:hide", (event) => {
if (event.target.matches("#unsaved-profile-panel") && hasUnsavedChanges()) {
event.preventDefault();
}
});
group.addEventListener("bs:collapse:shown", (event) => {
console.log("Opened panel", event.target.id, event.detail.controller);
});
Framework adapters
The Alpine, React, and Vue packages expose matching group-state helpers. Alpine binds the state directly; React and Vue feed each item's controlled state into their Collapse primitive. Keep the same Boobstrap classes and semantic heading-button-panel structure in every adapter.
Alpine.js
The Alpine plugin registers bsAccordion. Pass the initially open IDs first and an options object containing alwaysOpen second. Bind item(id) to the trigger and panel(id) to its panel.
<div class="bs-accordion"
x-data="bsAccordion(['shipping'], { alwaysOpen: true })"
data-bs-accordion data-bs-accordion-always-open>
<section class="bs-accordion-item">
<h2 class="bs-accordion-header">
<button class="bs-accordion-trigger" id="shipping-trigger" type="button"
aria-controls="shipping-panel" x-bind="item('shipping')">
Shipping
<i data-lucide="chevron-down" class="bs-accordion-icon bs-icon" aria-hidden="true"></i>
</button>
</h2>
<div class="bs-collapse bs-accordion-panel" id="shipping-panel"
aria-labelledby="shipping-trigger" x-bind="panel('shipping')">
<div class="bs-accordion-body">Shipping preferences.</div>
</div>
</section>
</div>
The provider exposes openIds, isOpen(id), setOpen(id, open), toggle(id), item(id), and panel(id). Use onOpenIdsChange(ids) in the options object to observe group changes.
React
useAccordion owns the set of open item IDs. Feed getItemOptions(id) into one useCollapse call per item so Collapse continues to own panel props and lifecycle events.
import { useAccordion, useCollapse } from "@boobstrap/react";
import { ChevronDown } from "lucide-react";
function AccordionItem({ accordion, id, title, children }) {
const collapse = useCollapse({ id: `${id}-panel`, ...accordion.getItemOptions(id) });
return (
<section className="bs-accordion-item">
<h2 className="bs-accordion-header">
<button className="bs-accordion-trigger"
id={`${id}-trigger`} {...collapse.getTriggerProps()}>
{title} <ChevronDown className="bs-accordion-icon bs-icon" aria-hidden="true" />
</button>
</h2>
<div className="bs-collapse bs-accordion-panel"
{...collapse.getPanelProps({
role: "region",
"aria-labelledby": `${id}-trigger`,
})}>
<div className="bs-accordion-body">{children}</div>
</div>
</section>
);
}
export function SettingsAccordion() {
const accordion = useAccordion({ defaultOpenIds: ["account"] });
return (
<div className="bs-accordion" {...accordion.getRootProps()}>
<AccordionItem accordion={accordion} id="account" title="Account">
Profile and sign-in preferences.
</AccordionItem>
<AccordionItem accordion={accordion} id="billing" title="Billing">
Payment methods and invoices.
</AccordionItem>
</div>
);
}
Add alwaysOpen: true for multiple open items. The hook also returns openIds, isOpen, and setOpen; onOpenIdsChange receives the next ID array.
Vue
Vue uses the same composition with refs: openIds and each Collapse open value are computed refs. Spread the prop helpers with v-bind.
<script setup>
import { useAccordion, useCollapse } from "@boobstrap/vue";
import { ChevronDown } from "lucide-vue-next";
const accordion = useAccordion({ defaultOpenIds: ["account"] });
const account = useCollapse({
id: "account-panel",
...accordion.getItemOptions("account"),
});
const billing = useCollapse({
id: "billing-panel",
...accordion.getItemOptions("billing"),
});
</script>
<template>
<div class="bs-accordion" v-bind="accordion.getRootProps()">
<section class="bs-accordion-item">
<h2 class="bs-accordion-header">
<button class="bs-accordion-trigger" id="account-trigger"
v-bind="account.getTriggerProps()">
Account <ChevronDown class="bs-accordion-icon bs-icon" aria-hidden="true" />
</button>
</h2>
<div class="bs-collapse bs-accordion-panel"
v-bind="account.getPanelProps({ 'aria-labelledby': 'account-trigger' })">
<div class="bs-accordion-body">Profile and sign-in preferences.</div>
</div>
</section>
<section class="bs-accordion-item">
<h2 class="bs-accordion-header">
<button class="bs-accordion-trigger" id="billing-trigger"
v-bind="billing.getTriggerProps()">
Billing <ChevronDown class="bs-accordion-icon bs-icon" aria-hidden="true" />
</button>
</h2>
<div class="bs-collapse bs-accordion-panel"
v-bind="billing.getPanelProps({ 'aria-labelledby': 'billing-trigger' })">
<div class="bs-accordion-body">Payment methods and invoices.</div>
</div>
</section>
</div>
</template>
For installation, controlled state, SSR, and shared lifecycle rules, see the React adapter guide and Vue adapter guide.
Reference
Classes
| Class | Element | Purpose |
|---|---|---|
.bs-accordion | Group root | Creates the bordered accordion surface. |
.bs-accordion-item | Item wrapper | Separates adjacent items with a logical block-start border. |
.bs-accordion-header | Heading | Removes the heading's default margin while preserving its semantics. |
.bs-accordion-trigger | Button | Provides full-width layout, hover treatment, and visible focus. |
.bs-accordion-icon | Decorative indicator | Rotates when its trigger has aria-expanded="true". |
.bs-collapse.bs-accordion-panel | Controlled panel | Combines interactive visibility state with accordion panel styling on the same element. |
.bs-accordion-body | Panel content wrapper | Applies inset spacing and muted text color. |
.bs-accordion-compact | Group root | Uses denser trigger and body spacing. |
.bs-accordion-flush | Group root | Removes inline outer borders and radius. |
Data attributes and state
| Attribute | Element | Contract |
|---|---|---|
data-bs-accordion | Group root | Opts the group into vanilla Accordion coordination. |
data-bs-accordion-always-open | Group root | Allows multiple child panels to remain open. |
data-bs-toggle="collapse" | Trigger | Connects click activation to the Collapse controller. |
aria-controls="panel-id" | Trigger | Identifies the controlled panel and associates the vanilla controller. |
aria-expanded | Trigger | Managed as the string true or false. |
hidden | Panel | Native source of truth for closed state in the vanilla controller. |
data-bs-state | Panel | Managed public state: open or closed. |
JavaScript API
| Contract | Returns | Purpose |
|---|---|---|
new Accordion(element) | Accordion | Coordinates identified Collapse panels inside one group. |
Accordion.getOrCreateInstance(element) | Accordion | Reuses the controller already associated with the element. |
accordion.alwaysOpen | boolean | Reads the group's current always-open attribute. |
accordion.destroy() | void | Removes group coordination and releases the cached Accordion instance. |
initAccordions(root?) | Accordion[] | Initializes matching groups at or below a document or element root. |
Use the Collapse controller's show(), hide(), and toggle() methods to change an individual item programmatically. Destroying an Accordion removes only the group listener; independently managed Collapse instances retain their own trigger listeners until they are destroyed.