Skip to documentation
BoobstrapDocs
Components

Accordion

Organize related disclosures into an accessible single-open or always-open group, using Collapse as the state and event layer.

v0.7.0 current Vanilla + adapters Copy-ready reference
Start here

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.

Update your profile, password, and sign-in preferences from one place.

HTML · Single-open group
<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.

Send a weekly activity summary.

HTML · Always-open, compact, and flush
<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>
Choose the mode by content, not layout. Use the default when each panel is an alternative view of the same topic. Use always-open when readers may need to compare answers or keep several controls visible.

Visual variants

Modifiers affect presentation only. They can be used separately or together and do not change state, events, or accessibility behavior.

ClassEffectGood fit
.bs-accordionBordered surface with large radius and standard spacing.Standalone FAQ and settings groups.
.bs-accordion-compactReduces trigger and body padding.Dense side panels and administrative screens.
.bs-accordion-flushRemoves the inline outer borders and corner radius.Accordions placed edge-to-edge inside another surface.

Accessibility

  • Use a native button for 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 id and point the trigger's aria-controls to it. Give the trigger its own id when the panel uses aria-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.

JavaScript · Initialize a 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.

JavaScript · Direct controller access
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();
Dynamically inserted items: initialize the new panel with 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.

EventCancelableWhen it fires
bs:collapse:showYesBefore a closed panel opens.
bs:collapse:shownNoAfter the panel is visible and public state is synchronized.
bs:collapse:hideYesBefore an open panel closes.
bs:collapse:hiddenNoAfter 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.

JavaScript · Validate before changing items
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.

HTML · Alpine accordion
<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.

JSX · React accordion
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.

Vue · Composition API accordion
<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

ClassElementPurpose
.bs-accordionGroup rootCreates the bordered accordion surface.
.bs-accordion-itemItem wrapperSeparates adjacent items with a logical block-start border.
.bs-accordion-headerHeadingRemoves the heading's default margin while preserving its semantics.
.bs-accordion-triggerButtonProvides full-width layout, hover treatment, and visible focus.
.bs-accordion-iconDecorative indicatorRotates when its trigger has aria-expanded="true".
.bs-collapse.bs-accordion-panelControlled panelCombines interactive visibility state with accordion panel styling on the same element.
.bs-accordion-bodyPanel content wrapperApplies inset spacing and muted text color.
.bs-accordion-compactGroup rootUses denser trigger and body spacing.
.bs-accordion-flushGroup rootRemoves inline outer borders and radius.

Data attributes and state

AttributeElementContract
data-bs-accordionGroup rootOpts the group into vanilla Accordion coordination.
data-bs-accordion-always-openGroup rootAllows multiple child panels to remain open.
data-bs-toggle="collapse"TriggerConnects click activation to the Collapse controller.
aria-controls="panel-id"TriggerIdentifies the controlled panel and associates the vanilla controller.
aria-expandedTriggerManaged as the string true or false.
hiddenPanelNative source of truth for closed state in the vanilla controller.
data-bs-statePanelManaged public state: open or closed.

JavaScript API

ContractReturnsPurpose
new Accordion(element)AccordionCoordinates identified Collapse panels inside one group.
Accordion.getOrCreateInstance(element)AccordionReuses the controller already associated with the element.
accordion.alwaysOpenbooleanReads the group's current always-open attribute.
accordion.destroy()voidRemoves 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.