Bootstrap Accordion: Add a Plus/Minus Toggle That Works
- The
collapsedclass on the button is your CSS hook β.accordion-button:not(.collapsed)::beforeis all you need to flip a plus to a minus with zero JavaScript. data-bs-parentmust be present on every singleaccordion-collapsediv 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 abootstrap.Collapseinstance 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.
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.
<!-- 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>
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.
data-bs-parent="#faqAccordion" 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.
<!-- 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>
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.
\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.
<!-- 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>
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.
show class) but its button has aria-expanded="false", 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="true" 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.
// 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. */
'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.
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.| Aspect | CSS-Only Plus/Minus (::before) | JavaScript-Driven Icon Swap |
|---|---|---|
| Implementation complexity | ~8 lines of CSS, zero JS | 20+ lines, event listeners required |
| Performance cost | Zero β pure CSS pseudo-element | Small: DOM query + listener on every toggle |
| Works without JS loaded | Yes β icon shows correct state even if JS fails | No β icon stuck in default state if JS errors |
| Custom icon libraries (Font Awesome, etc.) | Requires extra CSS class manipulation on pseudo-element | Straightforward β toggle classes directly on element |
| Animation options | Limited to CSS transitions on content property | Full control β can trigger any CSS class or animation |
| Bootstrap version coupling | Coupled to 'collapsed' class name (stable since BS4) | Coupled to bootstrap.Collapse API (stable since BS5) |
| Best for | Standard plus/minus text or simple Unicode icons | SVG icons, animated icons, or complex state logic |
π― Key Takeaways
- The
collapsedclass on the button is your CSS hook β.accordion-button:not(.collapsed)::beforeis all you need to flip a plus to a minus with zero JavaScript. data-bs-parentmust be present on every singleaccordion-collapsediv 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 abootstrap.Collapseinstance 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-expandedmismatches on page load β only after the first interaction. If your panel starts open,aria-expandedmust betruein 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 theshowclass must also haveclass="accordion-button collapsed". - βMistake 2: Omitting
data-bs-parenton one or more panels β symptom is multiple panels open simultaneously, no console error, users report 'accordion is broken'. Fix: grep everyaccordion-collapseelement in your HTML and confirmdata-bs-parentpoints 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 withnew bootstrap.Collapse(panel, { toggle: false }), or usebootstrap.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: '+' as the default, then override with content: '\2212' (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('collapseWarranty'); 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.
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.