Homeβ€Ί JavaScriptβ€Ί Bootstrap Accordion: Add a Plus/Minus Toggle That Works

Bootstrap Accordion: Add a Plus/Minus Toggle That Works

Where developers are forged. Β· Structured learning Β· Free forever.
πŸ“ Part of: HTML & CSS β†’ Topic 15 of 15
Bootstrap accordion plus/minus toggle done right β€” no jQuery hacks, no broken ARIA, just clean collapsible sections that work first time.
πŸ§‘β€πŸ’» Beginner-friendly β€” no prior JavaScript experience needed
In this tutorial, you'll learn:
  • The collapsed class on the button is your CSS hook β€” .accordion-button:not(.collapsed)::before is all you need to flip a plus to a minus with zero JavaScript.
  • data-bs-parent must be present on every single accordion-collapse div in your accordion β€” miss it on one panel and you get silent multi-open behaviour that only your users will notice.
  • Always pass { toggle: false } when constructing a bootstrap.Collapse instance in JavaScript β€” the constructor toggles the panel immediately by default, which will visually break your UI if you then call .show() or .hide() right after.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
⚑ Quick Answer
Picture a stack of filing cabinet drawers. Each drawer has a label on the front. You pull one open to see its contents, and the others stay shut β€” the cabinet doesn't explode every drawer at once just because you touched one. A Bootstrap accordion is exactly that for a webpage: a stack of labelled panels where clicking one opens it and the rest stay collapsed. The plus/minus icon is just the handle on the drawer β€” it tells you at a glance whether a drawer is open or shut.

The most common Bootstrap accordion bug I've debugged across five different client projects wasn't a JavaScript error β€” it was a plus sign that never became a minus sign, because someone copy-pasted the HTML from the docs and didn't read the part about data attributes. The accordion opened and closed just fine, but the icon stayed frozen as a plus forever. Users assumed it was broken and called support. That was a $40/hour support ticket for six lines of CSS that should have been there from day one.

HTML pages with dense content β€” FAQs, product details, settings panels β€” have a real problem: they dump everything on screen at once and users scroll endlessly looking for the one thing they need. Before accordions existed as a proper pattern, developers were writing raw jQuery show/hide spaghetti, duplicating event listeners, and forgetting to handle keyboard navigation entirely. Accessibility auditors had a field day. The Bootstrap accordion component solves this with a consistent, accessible, keyboard-navigable collapsible panel system β€” and it does it without a single line of custom JavaScript if you wire it up correctly.

By the end of this article you'll be able to build a fully working Bootstrap 5 accordion with a real plus/minus icon that flips state on open and close, understand exactly which HTML attributes control the collapse behaviour, and know the three mistakes that make accordions silently break in production β€” before you ship them.

How Bootstrap's Collapse Engine Actually Works Under the Hood

Before you touch a single accordion, you need to understand what's actually driving it β€” because the moment you don't, you'll spend an afternoon wondering why your panel won't open and the console is completely silent.

Bootstrap's accordion is built on top of its Collapse plugin. That plugin watches for click events on any element that has a data-bs-toggle="collapse" attribute. When clicked, it finds the target element β€” identified by data-bs-target or href β€” and toggles the CSS classes show and collapsed on the relevant elements. That's it. There's no magic. It's a class toggler with some CSS transitions baked in.

The accordion layer adds one rule on top: data-bs-parent. Set this attribute to the ID of the accordion's wrapper element, and Bootstrap will automatically close any currently open panel when you open a new one. Without data-bs-parent, every panel opens independently β€” which is sometimes what you want, but not what most people picture when they say 'accordion'.

Why does this matter practically? Because if you forget data-bs-parent on even one panel, that panel will open alongside others instead of replacing them. Users see two panels open at once, assume the component is broken, and your UX scores take a hit. I've seen this exact bug ship to production because a developer added a fourth panel to an existing three-panel accordion and didn't copy the attribute.

AccordionStructure.html Β· HTML
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
<!-- io.thecodeforge β€” JavaScript tutorial -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Bootstrap Accordion β€” TheCodeForge</title>

  <!-- Bootstrap 5 CSS from CDN β€” no npm required for this demo -->
  <link
    rel="stylesheet"
    href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
  />
</head>
<body class="p-4">

  <!--
    WRAPPER: id="faqAccordion" is the anchor point for data-bs-parent.
    Every panel inside MUST reference this ID or the
    'only one open at a time' behaviour will silently break.
  -->
  <div class="accordion" id="faqAccordion">

    <!-- ── PANEL 1 ─────────────────────────────────────── -->
    <div class="accordion-item">

      <!--
        accordion-header wraps the button.
        The id here ("headingShipping") is referenced by
        aria-labelledby on the body panel below β€” required for screen readers.
      -->
      <h2 class="accordion-header" id="headingShipping">
        <button
          class="accordion-button"
          type="button"
          data-bs-toggle="collapse"
          data-bs-target="#collapseShipping"
          aria-expanded="true"
          aria-controls="collapseShipping"
        >
          How long does shipping take?
        </button>
      </h2>

      <!--
        accordion-collapse is the panel body wrapper.
        'show' class = visible on page load (matches aria-expanded="true" above).
        data-bs-parent="#faqAccordion" is what enforces mutual exclusivity.
        NEVER omit this β€” see callout below.
      -->
      <div
        id="collapseShipping"
        class="accordion-collapse collapse show"
        aria-labelledby="headingShipping"
        data-bs-parent="#faqAccordion"
      >
        <div class="accordion-body">
          Standard shipping takes 3–5 business days.
          Express options are available at checkout.
        </div>
      </div>
    </div>
    <!-- ── END PANEL 1 ──────────────────────────────────── -->

    <!-- ── PANEL 2 ─────────────────────────────────────── -->
    <div class="accordion-item">
      <h2 class="accordion-header" id="headingReturns">
        <!--
          No 'show' class on this button's panel = starts collapsed.
          aria-expanded="false" must match β€” Bootstrap won't auto-correct this.
        -->
        <button
          class="accordion-button collapsed"
          type="button"
          data-bs-toggle="collapse"
          data-bs-target="#collapseReturns"
          aria-expanded="false"
          aria-controls="collapseReturns"
        >
          What is your return policy?
        </button>
      </h2>
      <div
        id="collapseReturns"
        class="accordion-collapse collapse"
        aria-labelledby="headingReturns"
        data-bs-parent="#faqAccordion"
      >
        <div class="accordion-body">
          Returns are accepted within 30 days of delivery.
          Items must be unused and in original packaging.
        </div>
      </div>
    </div>
    <!-- ── END PANEL 2 ──────────────────────────────────── -->

  </div>
  <!-- ── END ACCORDION WRAPPER ───────────────────────────── -->

  <!-- Bootstrap 5 JS bundle (includes Popper) β€” must be AFTER the HTML -->
  <script
    src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js">
  </script>

</body>
</html>
β–Ά Output
Page renders with two accordion panels.
Panel 1 ('How long does shipping take?') is open by default.
Panel 2 ('What is your return policy?') is collapsed.
Clicking Panel 2 opens it and simultaneously closes Panel 1.
Clicking Panel 1 again reopens it and closes Panel 2.
Keyboard navigation (Tab + Enter/Space) works without any custom JS.
⚠️
Production Trap: Missing data-bs-parent Breaks Mutual Exclusivity SilentlyIf you omit data-bs-parent=&quot;#faqAccordion&quot; from even one panel's accordion-collapse div, that panel will open alongside others instead of replacing them. The console shows zero errors. The only symptom is two panels open at once β€” which users report as 'the accordion is broken'. Fix: grep your accordion HTML for every accordion-collapse element and confirm every single one carries the matching data-bs-parent attribute.

The Plus/Minus Toggle: Why Bootstrap Doesn't Give You This for Free

Here's something the Bootstrap docs bury in a footnote: the default accordion uses a CSS chevron (β€Ί) that rotates on open/close β€” it does NOT give you a plus/minus toggle out of the box. If your design calls for a + when collapsed and a βˆ’ when expanded, you're doing that yourself. That's not a criticism of Bootstrap; it's just how it is. Know it going in.

The mechanism Bootstrap does give you for free is the collapsed class. When a panel is closed, the trigger button carries the class collapsed. When it's open, that class is absent. You can hook into this with CSS to swap any icon you want β€” no JavaScript required.

The cleanest production approach is to use CSS content on a pseudo-element tied to the button's state. You put your plus character as the default content, and you override it to a minus when the collapsed class is absent (meaning the panel is open). This is a single CSS rule pair. It costs you nothing at runtime, it works with Bootstrap's existing class-toggling behaviour, and it survives framework upgrades because it relies on the documented collapsed class β€” not internal Bootstrap implementation details that change between minor versions.

AccordionPlusMinus.html Β· HTML
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
<!-- io.thecodeforge β€” JavaScript tutorial -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>FAQ Accordion β€” Plus/Minus Toggle</title>

  <link
    rel="stylesheet"
    href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
  />

  <style>
    /*
     * Step 1: Kill Bootstrap's default rotating chevron.
     * Bootstrap injects it via .accordion-button::after using a background-image.
     * Setting background-image to none removes it entirely.
     * We also clear the transform so Bootstrap's rotate animation doesn't fire.
     */
    .accordion-button::after {
      background-image: none !important;
      transform: none !important;
      content: '';
    }

    /*
     * Step 2: Create our own icon element via ::before.
     * We use ::before (not ::after) so the icon sits on the LEFT
     * of the button text, matching the design spec for this example.
     * Swap to ::after and adjust margin if your design puts it on the right.
     */
    .accordion-button::before {
      /*
       * Default state = button is COLLAPSED = panel is CLOSED.
       * Show a plus sign.
       */
      content: '+';
      font-size: 1.25rem;
      font-weight: 700;
      line-height: 1;
      margin-right: 0.75rem;

      /*
       * Prevent the icon from shifting layout when it changes character.
       * A minus sign is narrower than a plus in most fonts β€” inline-block
       * with a fixed width keeps the text from jumping left on open.
       */
      display: inline-block;
      width: 1rem;
      text-align: center;

      /*
       * Smooth the character swap with opacity β€” purely cosmetic but
       * removes the jarring instant change that looks like a bug.
       */
      transition: opacity 0.15s ease-in-out;
    }

    /*
     * Step 3: When the button does NOT have the 'collapsed' class,
     * the panel is OPEN. Swap the plus for a minus.
     *
     * Key insight: Bootstrap ADDS 'collapsed' when closing and REMOVES it
     * when opening. So 'no collapsed class' = open state.
     * We target the ABSENCE of the class, not its presence.
     */
    .accordion-button:not(.collapsed)::before {
      content: '\2212'; /* Unicode minus sign β€” visually wider than a hyphen */
    }

    /* Optional: remove Bootstrap's default blue focus outline colour change */
    .accordion-button:not(.collapsed) {
      color: inherit;
      background-color: #f8f9fa;
      box-shadow: none;
    }
  </style>
</head>
<body class="p-4">

  <h1 class="mb-4">Frequently Asked Questions</h1>

  <div class="accordion" id="productFaqAccordion">

    <!-- ── PANEL 1: Starts OPEN ────────────────────────── -->
    <div class="accordion-item">
      <h2 class="accordion-header" id="headingPayment">
        <button
          class="accordion-button"
          type="button"
          data-bs-toggle="collapse"
          data-bs-target="#collapsePayment"
          aria-expanded="true"
          aria-controls="collapsePayment"
        >
          What payment methods do you accept?
        </button>
      </h2>
      <div
        id="collapsePayment"
        class="accordion-collapse collapse show"
        aria-labelledby="headingPayment"
        data-bs-parent="#productFaqAccordion"
      >
        <div class="accordion-body">
          We accept Visa, Mastercard, PayPal, and bank transfer.
          All transactions are encrypted with TLS 1.3.
        </div>
      </div>
    </div>

    <!-- ── PANEL 2: Starts COLLAPSED ─────────────────────── -->
    <div class="accordion-item">
      <h2 class="accordion-header" id="headingWarranty">
        <!--
          'collapsed' class is present here because this panel starts closed.
          This means our CSS default (plus sign) shows immediately on load.
          If you forget this class, the button shows a minus sign on load
          even though the panel is closed β€” that's the classic mismatch bug.
        -->
        <button
          class="accordion-button collapsed"
          type="button"
          data-bs-toggle="collapse"
          data-bs-target="#collapseWarranty"
          aria-expanded="false"
          aria-controls="collapseWarranty"
        >
          Does my purchase include a warranty?
        </button>
      </h2>
      <div
        id="collapseWarranty"
        class="accordion-collapse collapse"
        aria-labelledby="headingWarranty"
        data-bs-parent="#productFaqAccordion"
      >
        <div class="accordion-body">
          All hardware products carry a 24-month manufacturer warranty.
          Software licences are covered under our 30-day satisfaction guarantee.
        </div>
      </div>
    </div>

    <!-- ── PANEL 3: Starts COLLAPSED ─────────────────────── -->
    <div class="accordion-item">
      <h2 class="accordion-header" id="headingCancellation">
        <button
          class="accordion-button collapsed"
          type="button"
          data-bs-toggle="collapse"
          data-bs-target="#collapseCancellation"
          aria-expanded="false"
          aria-controls="collapseCancellation"
        >
          Can I cancel my subscription at any time?
        </button>
      </h2>
      <div
        id="collapseCancellation"
        class="accordion-collapse collapse"
        aria-labelledby="headingCancellation"
        data-bs-parent="#productFaqAccordion"
      >
        <div class="accordion-body">
          Yes. Cancel from your account dashboard any time.
          You keep access until the end of your billing period.
        </div>
      </div>
    </div>

  </div>

  <script
    src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js">
  </script>

</body>
</html>
β–Ά Output
Page loads with Panel 1 open, showing a 'βˆ’' icon on its button.
Panels 2 and 3 are collapsed, each showing a '+' icon.
Clicking Panel 2: Panel 1 closes (icon flips to '+'), Panel 2 opens (icon flips to 'βˆ’').
Clicking Panel 2 again: Panel 2 closes (icon returns to '+'), panel stays closed.
Icon transitions have a 0.15s opacity fade.
No JavaScript written. No jQuery. Zero custom event listeners.
⚠️
Senior Shortcut: Use Unicode \2212 for Minus, Not a HyphenThe hyphen character (-) is narrower than a plus sign (+) in almost every font. Using a hyphen as your 'minus' makes the button text visually jump left when the panel opens because the icon shrinks. Use the actual Unicode minus sign \2212 in your CSS content property β€” it's the same width as a plus and looks intentional rather than hacked together.

Accessibility and the ARIA Attributes You Can't Skip

I've watched a Bootstrap accordion sail through QA and get flagged on the first accessibility audit because the developer treated aria-expanded as decorative. It's not. Screen readers use aria-expanded to announce whether a panel is open or closed β€” without it, a visually impaired user hears a button with no context and has to guess what it controls.

The good news: Bootstrap's Collapse plugin automatically toggles aria-expanded for you between true and false as panels open and close. Your only job is to make sure the initial HTML state is honest. If a panel starts collapsed, aria-expanded must be false on load. If it starts open, it must be true. Bootstrap doesn't backfill this on page load β€” it only manages it after the first interaction.

The other attribute people skip is aria-controls. This tells assistive technology which element the button controls β€” it should match the id of the accordion-collapse div. And the aria-labelledby on the collapse div should point back to the accordion-header id. These two form a two-way relationship that lets screen readers navigate the accordion as a coherent structure rather than a pile of unrelated buttons and divs. Skip either one and you'll fail WCAG 2.1 Level AA β€” which matters if you're building anything for a government contract, healthcare platform, or any company that takes accessibility law seriously.

AccordionAccessibility.html Β· HTML
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
<!-- io.thecodeforge β€” JavaScript tutorial -->

<!--
  This shows the complete ARIA relationship map for one accordion panel.
  Annotated specifically to show what each accessibility attribute does
  and what breaks if you remove it.
-->

<div class="accordion" id="settingsAccordion">

  <div class="accordion-item">

    <!--
      aria-labelledby on the collapse div below will point to THIS id.
      The heading id must be unique per page β€” not just per accordion.
      Using a descriptive id ('headingNotifications') beats 'heading1'
      because it's readable in accessibility tree inspectors.
    -->
    <h2 class="accordion-header" id="headingNotifications">

      <button
        class="accordion-button collapsed"
        type="button"
        data-bs-toggle="collapse"
        data-bs-target="#collapseNotifications"

        <!--
          aria-expanded MUST match visual state on load.
          'false' here = panel is visually collapsed = correct.
          Bootstrap updates this attribute dynamically after first click.
          If you ship aria-expanded="false" on an open panel,
          screen readers announce it as closed β€” users can't trust the page.
        -->
        aria-expanded="false"

        <!--
          aria-controls = id of the panel this button controls.
          Screen readers use this to let users jump directly to the
          content without having to tab through every element.
        -->
        aria-controls="collapseNotifications"
      >
        Notification Preferences
      </button>
    </h2>

    <div
      id="collapseNotifications"
      class="accordion-collapse collapse"

      <!--
        aria-labelledby creates the reverse link:
        this panel announces it is labelled by the heading button above.
        Must match the accordion-header's id exactly β€” case-sensitive.
      -->
      aria-labelledby="headingNotifications"

      data-bs-parent="#settingsAccordion"
    >
      <div class="accordion-body">
        <p>Choose how and when you receive alerts.</p>

        <!-- Real settings form would live here -->
        <div class="form-check">
          <input class="form-check-input" type="checkbox" id="emailAlerts" />
          <label class="form-check-label" for="emailAlerts">
            Email alerts for order updates
          </label>
        </div>

        <div class="form-check mt-2">
          <input class="form-check-input" type="checkbox" id="smsAlerts" />
          <label class="form-check-label" for="smsAlerts">
            SMS alerts for shipping changes
          </label>
        </div>
      </div>
    </div>

  </div>
</div>
β–Ά Output
Panel starts collapsed β€” button reads: 'Notification Preferences, collapsed, button' to screen readers.
After clicking: 'Notification Preferences, expanded, button' β€” Bootstrap flips aria-expanded automatically.
Keyboard: Tab to button, Space or Enter to toggle. No mouse required.
Accessibility tree shows button controls 'collapseNotifications' via aria-controls.
Panel announces its label as 'Notification Preferences' via aria-labelledby.
⚠️
Never Do This: aria-expanded Mismatch on Page LoadIf a panel starts visually open (has the show class) but its button has aria-expanded=&quot;false&quot;, screen readers announce the panel as closed. Users navigate away thinking there's no content. Bootstrap will NOT fix this mismatch on load β€” it only manages aria-expanded after the first user interaction. The symptom is invisible in a browser but will fail any WCAG audit instantly. Rule: show class on the panel = aria-expanded=&quot;true&quot; on the button. No exceptions.

Controlling the Accordion with JavaScript When HTML Attributes Aren't Enough

Data attributes get you 90% of the way there. But there are real production scenarios where you need JavaScript control β€” opening a specific panel based on a URL hash, programmatically closing all panels when a user submits a form, or listening to open/close events to trigger analytics tracking.

Bootstrap exposes a JavaScript API for the Collapse component. You get a bootstrap.Collapse instance either by constructing one with new bootstrap.Collapse(element, options) or by grabbing one that already exists with bootstrap.Collapse.getInstance(element). The key methods are .show(), .hide(), and .toggle(). The key events are show.bs.collapse, shown.bs.collapse, hide.bs.collapse, and hidden.bs.collapse β€” fired on the collapse element itself, not on the button.

One thing that trips people up: if you construct a new bootstrap.Collapse(element) without passing { toggle: false }, Bootstrap immediately toggles the panel on construction. That means creating an instance to attach an event listener unintentionally opens the panel. Always pass { toggle: false } when you're constructing an instance purely to get programmatic control or listen to events.

AccordionJavaScriptControl.js Β· JAVASCRIPT
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
// io.thecodeforge β€” JavaScript tutorial

// ─── SCENARIO ───────────────────────────────────────────────────────────────
// A product page accordion where:
// 1. The correct panel auto-opens based on the URL hash (e.g. #collapseWarranty)
// 2. All panels can be force-closed when the user hits 'Reset filters'
// 3. A 'panel opened' event is sent to analytics
// ─────────────────────────────────────────────────────────────────────────────

document.addEventListener('DOMContentLoaded', function () {

  // ── 1. OPEN PANEL FROM URL HASH ──────────────────────────────────────────
  //
  // If the URL is /product#collapseWarranty, open that panel automatically.
  // Useful for deep-linking from emails or support articles.

  const urlHash = window.location.hash; // e.g. "#collapseWarranty"

  if (urlHash) {
    const targetPanel = document.querySelector(urlHash);

    if (targetPanel && targetPanel.classList.contains('accordion-collapse')) {
      /*
       * { toggle: false } is critical here.
       * Without it, constructing the instance triggers an immediate toggle β€”
       * which would open AND THEN CLOSE the panel before the user sees anything.
       * With toggle: false, we construct the instance without touching panel state.
       */
      const collapseInstance = new bootstrap.Collapse(targetPanel, {
        toggle: false
      });

      // Now explicitly open it β€” clean, predictable, one-way
      collapseInstance.show();
    }
  }

  // ── 2. FORCE-CLOSE ALL PANELS ON 'RESET' ────────────────────────────────
  //
  // A 'Reset filters' button that also collapses all open accordion panels
  // to return the UI to its default state.

  const resetButton = document.getElementById('resetFiltersButton');

  if (resetButton) {
    resetButton.addEventListener('click', function () {

      // Grab every open panel (has the 'show' class)
      const openPanels = document.querySelectorAll(
        '#productFaqAccordion .accordion-collapse.show'
      );

      openPanels.forEach(function (panel) {
        /*
         * getInstance returns the existing Collapse instance if Bootstrap
         * already manages this element. Returns null if no instance exists yet.
         * Using getInstance avoids creating duplicate instances,
         * which can cause double-fire events in Bootstrap 5.
         */
        const existingInstance = bootstrap.Collapse.getInstance(panel);

        if (existingInstance) {
          existingInstance.hide();
        } else {
          // No instance yet β€” create one just to hide, without toggling on construct
          new bootstrap.Collapse(panel, { toggle: false }).hide();
        }
      });
    });
  }

  // ── 3. ANALYTICS EVENT ON PANEL OPEN ─────────────────────────────────────
  //
  // Fire a tracking event every time a panel finishes opening.
  // Using 'shown.bs.collapse' (past tense) rather than 'show.bs.collapse'
  // means the animation has completed β€” the panel is fully visible.
  // This avoids tracking panels the user flicked open and immediately closed
  // before the animation finished.

  const accordionWrapper = document.getElementById('productFaqAccordion');

  if (accordionWrapper) {
    accordionWrapper.addEventListener('shown.bs.collapse', function (event) {
      /*
       * event.target is the collapse panel element that just opened.
       * Its id is the panel identifier we use to label the analytics event.
       */
      const openedPanelId = event.target.id;

      // Replace with your actual analytics call (GA4, Segment, Mixpanel, etc.)
      console.log('[Analytics] Accordion panel opened:', openedPanelId);

      /*
       * Real-world example:
       * window.dataLayer.push({
       *   event: 'accordion_panel_opened',
       *   panel_id: openedPanelId,
       *   page_path: window.location.pathname
       * });
       */
    });
  }

});

/*
 * BOOTSTRAP COLLAPSE EVENTS β€” QUICK REFERENCE
 * ─────────────────────────────────────────────────────────────
 * show.bs.collapse    β€” fires when show() is called (animation starts)
 * shown.bs.collapse   β€” fires when panel is fully open (animation ends)
 * hide.bs.collapse    β€” fires when hide() is called (animation starts)
 * hidden.bs.collapse  β€” fires when panel is fully closed (animation ends)
 *
 * All four fire on the PANEL ELEMENT, not on the trigger button.
 * Delegate from the accordion wrapper to catch all panels in one listener.
 */
β–Ά Output
URL /product#collapseWarranty β†’ Warranty panel opens automatically on load.
'Reset filters' button click β†’ all open panels collapse cleanly.
Each time a panel finishes opening β†’ console logs:
[Analytics] Accordion panel opened: collapseWarranty
[Analytics] Accordion panel opened: collapsePayment
No duplicate instances. No double-fire events. No unintentional toggles on construction.
⚠️
The Classic Bug: new bootstrap.Collapse() Without { toggle: false }Every time I've seen a Bootstrap panel mysteriously open and immediately close on its own, the culprit is new bootstrap.Collapse(element) without { toggle: false }. Bootstrap treats construction as implicit activation β€” so it toggles the panel the moment you create the instance. Then your .show() call toggles it again, closing it. Always pass { toggle: false } when constructing an instance for programmatic control. Always use bootstrap.Collapse.getInstance() first to avoid creating a second instance on an element Bootstrap already manages.
AspectCSS-Only Plus/Minus (::before)JavaScript-Driven Icon Swap
Implementation complexity~8 lines of CSS, zero JS20+ lines, event listeners required
Performance costZero β€” pure CSS pseudo-elementSmall: DOM query + listener on every toggle
Works without JS loadedYes β€” icon shows correct state even if JS failsNo β€” icon stuck in default state if JS errors
Custom icon libraries (Font Awesome, etc.)Requires extra CSS class manipulation on pseudo-elementStraightforward β€” toggle classes directly on element
Animation optionsLimited to CSS transitions on content propertyFull control β€” can trigger any CSS class or animation
Bootstrap version couplingCoupled to 'collapsed' class name (stable since BS4)Coupled to bootstrap.Collapse API (stable since BS5)
Best forStandard plus/minus text or simple Unicode iconsSVG icons, animated icons, or complex state logic

🎯 Key Takeaways

  • The collapsed class on the button is your CSS hook β€” .accordion-button:not(.collapsed)::before is all you need to flip a plus to a minus with zero JavaScript.
  • data-bs-parent must be present on every single accordion-collapse div in your accordion β€” miss it on one panel and you get silent multi-open behaviour that only your users will notice.
  • Always pass { toggle: false } when constructing a bootstrap.Collapse instance in JavaScript β€” the constructor toggles the panel immediately by default, which will visually break your UI if you then call .show() or .hide() right after.
  • Bootstrap does not fix aria-expanded mismatches on page load β€” only after the first interaction. If your panel starts open, aria-expanded must be true in the raw HTML or screen readers will announce the page incorrectly from the first second.

⚠ Common Mistakes to Avoid

  • βœ•Mistake 1: Forgetting the 'collapsed' class on the button for panels that start closed β€” the button renders with a minus icon on page load even though the panel is shut, because the CSS targets .accordion-button:not(.collapsed) β€” the minus state. Fix: every button whose panel starts without the show class must also have class=&quot;accordion-button collapsed&quot;.
  • βœ•Mistake 2: Omitting data-bs-parent on one or more panels β€” symptom is multiple panels open simultaneously, no console error, users report 'accordion is broken'. Fix: grep every accordion-collapse element in your HTML and confirm data-bs-parent points to the correct wrapper ID. It must be present on every panel, not just the first.
  • βœ•Mistake 3: Calling new bootstrap.Collapse(panel) without { toggle: false } inside a click handler β€” panel opens and immediately closes, appearing to ignore the click. The constructor triggers one toggle and your explicit .show() call triggers a second. Fix: always construct with new bootstrap.Collapse(panel, { toggle: false }), or use bootstrap.Collapse.getInstance(panel) to retrieve the existing managed instance instead of creating a new one.

Interview Questions on This Topic

  • QBootstrap's accordion relies on the 'collapsed' CSS class to manage icon state. What happens if a developer initialises an accordion panel as open in the HTML but forgets to remove the 'collapsed' class from the trigger button β€” and how does this affect both the visual icon and screen reader behaviour?
  • QWhen would you choose to implement accordion open/close behaviour with vanilla CSS :focus-within or the HTML details/summary element instead of Bootstrap's Collapse component β€” and what's the concrete trade-off in each case for a production FAQ page?
  • QIf you fire new bootstrap.Collapse(element).show() on a panel that Bootstrap is already managing via data attributes, what specific event is fired twice and what is the observable symptom in the UI β€” and how do you prevent it?

Frequently Asked Questions

How do I add a plus and minus icon to a Bootstrap 5 accordion without JavaScript?

Use CSS pseudo-elements on .accordion-button::before. Set content: &#39;+&#39; as the default, then override with content: &#39;\2212&#39; (Unicode minus) on .accordion-button:not(.collapsed)::before. Bootstrap automatically adds and removes the collapsed class on the button as panels open and close β€” your CSS reacts to that class change. No event listeners needed.

What's the difference between the 'show' class and the 'collapsed' class in Bootstrap accordion?

They live on different elements and serve different purposes. The show class goes on the accordion-collapse div (the panel body) and controls whether the panel content is visible. The collapsed class goes on the accordion-button (the trigger) and signals that the button's associated panel is currently closed. They're managed together by Bootstrap, but they're on separate HTML elements β€” confusing them is the number one cause of icon-state bugs.

How do I open a specific Bootstrap accordion panel automatically on page load using JavaScript?

Grab the panel element by ID, construct a Collapse instance with { toggle: false } to prevent the constructor from toggling state, then call .show(). Full pattern: const panel = document.getElementById(&#39;collapseWarranty&#39;); new bootstrap.Collapse(panel, { toggle: false }).show();. Run this inside a DOMContentLoaded listener to ensure Bootstrap has initialised. If you want URL-hash-based auto-open, read window.location.hash and target that element ID.

Why does my Bootstrap accordion open multiple panels at the same time instead of closing the previous one?

Every accordion-collapse div needs data-bs-parent pointing to the wrapper accordion's ID β€” not just the first one. Bootstrap's mutual-exclusivity logic only kicks in for panels that carry this attribute. If you added a new panel to an existing accordion and didn't include data-bs-parent, that new panel will open independently alongside others. Check every single accordion-collapse element in your markup β€” it's almost always a copy-paste omission on a later-added panel.

πŸ”₯
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousCSS Quotes: Styling Quotation Marks with CSS
Forged with πŸ”₯ at TheCodeForge.io β€” Where Developers Are Forged