Tips & Tricks

Bootstrap 5 Tabs to Accordions with Persistence, the Holy Grail?

Here's our take on the design pattern that's often tricky to get right...

84 views

Demo

Resize viewport < 768px or in Firefox, press CTRL + SHIFT + M to enter responsive design mode.

Content for Section 1

Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

Content for Section 2

Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.

Content for Section 3

Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.

Markup

<div class="container my-3">
  <ul id="myTab" class="nav nav-tabs justify-content-center d-none d-md-flex border p-3 pb-0 rounded-top border-bottom-0 bg-body-tertiary" role="tablist">
    <li class="responsive nav-item px-1" role="presentation"><button id="tab-1" class="responsive nav-link text-reset p-3 py-2 fs-4 fw-semibold active" role="tab" type="button" data-bs-toggle="tab" data-bs-target="#pane-1">Section 1</button></li>
    <li class="responsive nav-item px-1" role="presentation"><button id="tab-2" class="responsive nav-link text-reset p-3 py-2 fs-4 fw-semibold" role="tab" type="button" data-bs-toggle="tab" data-bs-target="#pane-2">Section 2</button></li>
    <li class="responsive nav-item px-1" role="presentation"><button id="tab-3" class="responsive nav-link text-reset p-3 py-2 fs-4 fw-semibold" role="tab" type="button" data-bs-toggle="tab" data-bs-target="#pane-3">Section 3</button></li>
  </ul>
  <div class="responsive tab-content accordion" id="mainTabContent">
    <div class="responsive accordion-item">
      <div class="responsive accordion-header d-md-none"><button class="responsive accordion-button plus p-2 fs-3 fw-semibold collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#pane-1"> Section 1 </button></div>
      <div id="pane-1" class="responsive tab-pane show active accordion-collapse collapse" data-bs-parent="#mainTabContent">
        <div class="responsive accordion-body p-3">
          <h3 class="mt-0">Content for Section 1</h3>
          <p class="m-0">Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.</p>
        </div>
      </div>
    </div>
    <div class="responsive accordion-item">
      <div class="responsive accordion-header d-md-none"><button class="responsive accordion-button plus p-2 fs-3 fw-semibold collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#pane-2"> Section 2 </button></div>
      <div id="pane-2" class="responsive tab-pane accordion-collapse collapse" data-bs-parent="#mainTabContent">
        <div class="responsive accordion-body p-3">
          <h3 class="mt-0">Content for Section 2</h3>
          <p class="m-0">Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.</p>
        </div>
      </div>
    </div>
    <div class="responsive accordion-item">
      <div class="responsive accordion-header d-md-none"><button class="responsive accordion-button plus p-2 fs-3 fw-semibold collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#pane-3"> Section 3 </button></div>
      <div id="pane-3" class="responsive tab-pane accordion-collapse collapse" data-bs-parent="#mainTabContent">
        <div class="responsive accordion-body p-3">
          <h3 class="mt-0">Content for Section 3</h3>
          <p class="m-0">Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.</p>
        </div>
      </div>
    </div>
  </div>
</div>

CSS

@media (min-width: 768px) {
  .responsive.accordion-item {
    border: none !important;
    border-bottom-left-radius: var(--bs-border-radius) !important;
    border-bottom-right-radius: var(--bs-border-radius) !important;
  }
  .responsive.accordion-body {
    border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;
    border-bottom-left-radius: var(--bs-border-radius) !important;
    border-bottom-right-radius: var(--bs-border-radius) !important;
  }
}
.responsive.accordion-button:not(.collapsed),
.responsive.accordion-body .accordion-button:not(.collapsed) {
  color: var(--bs-accordion-active-color) !important;
  background-color: var(--bs-tertiary-bg) !important;
  border-bottom: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;
}
.responsive.accordion-button {
  box-shadow: none !important;
  transition: none !important;
}

[data-bs-theme="dark"] .accordion-button::after {
  --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23ffffff'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e");
  --bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23ffffff'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e");
}

.accordion-button.plus::after {
  --bs-accordion-btn-icon-width: 2rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23000' class='bi bi-plus' viewBox='0 0 16 16'%3E%3Cpath d='M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z'/%3E%3C/svg%3E") !important;
  transition: all 0.5s;
}
.accordion-button.plus:not(.collapsed)::after {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23000' class='bi bi-dash' viewBox='0 0 16 16'%3E%3Cpath d='M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8z'/%3E%3C/svg%3E") !important;
}

[data-bs-theme="dark"] .accordion-button.plus::after {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23fff' class='bi bi-plus' viewBox='0 0 16 16'%3E%3Cpath d='M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z'/%3E%3C/svg%3E") !important;
  transition: all 0.5s;
}
[data-bs-theme="dark"] .accordion-button.plus:not(.collapsed)::after {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23fff' class='bi bi-dash' viewBox='0 0 16 16'%3E%3Cpath d='M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8z'/%3E%3C/svg%3E") !important;
}

JavaScript

function debounce(fn, wait) {
    let timer;
    return function (...args) {
        clearTimeout(timer);
        timer = setTimeout(() => fn.apply(this, args), wait);
    };
}

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

    const storageKey = "activeTabSetting";
    const tabPanes = document.querySelectorAll('.responsive.tab-pane');
    const tabButtons = document.querySelectorAll('.responsive[data-bs-toggle="tab"]');
    const accordionButtons = document.querySelectorAll('.responsive.accordion-button');
    
    const scrollToTopAcc = false; // Scroll selected accordion item into view
    const oneAccAlwaysOpen = false; // One accordion item always open

    // Load saved state
    function loadSavedState() {
    const savedTarget = localStorage.getItem(storageKey);
    if (savedTarget && document.querySelectorAll(savedTarget).length) {
        const targetEl = document.querySelector(savedTarget);
        if (targetEl) {
            // Reset actives
            document.querySelectorAll('.responsive.active, .responsive.show').forEach(el => el.classList.remove('active', 'show'));
            // Activate Pane
            targetEl.classList.add('active', 'show');
            // Activate Tab Button
            const tabBtn = document.querySelector(`[data-bs-target="${savedTarget}"][data-bs-toggle="tab"]`);
            if (tabBtn) tabBtn.classList.add('active');
            // Activate Accordion Button
            const accBtn = document.querySelector(`[data-bs-target="${savedTarget}"].responsive.accordion-button`);
            const accordionButtons = document.querySelectorAll('.responsive.accordion-button');
            accordionButtons.forEach(btn => { btn.classList.add('collapsed'); });
            if (accBtn) accBtn.classList.remove('collapsed');
       }
    }
    }
	loadSavedState();
	window.addEventListener('resize', debounce(loadSavedState, 100));

    // Update localStorage on change
    function updateState(targetId) {
        localStorage.setItem(storageKey, targetId);
        // Ensure that if we click an accordion, the tab also looks active (and vice versa)
        tabButtons.forEach(btn => {
            btn.classList.toggle('active', btn.getAttribute('data-bs-target') === targetId);
        });
    }

    // Listen for tab clicks
    tabButtons.forEach(btn => {
        btn.addEventListener('shown.bs.tab', e => updateState(e.target.getAttribute('data-bs-target')));
    });

    // Listen for accordion clicks
    accordionButtons.forEach(btn => {
        btn.addEventListener('click', function() {
            const targetId = this.getAttribute('data-bs-target');
            updateState(targetId);
            // Scroll to top of accordion
            if (scrollToTopAcc === true) {
            setTimeout(() => {
                const element = document.querySelector(targetId);
                const offset = 60; // Padding from top
                const bodyRect = document.body.getBoundingClientRect().top;
                const elementRect = element.getBoundingClientRect().top;
                const elementPosition = elementRect - bodyRect;
                const offsetPosition = elementPosition - offset;
                window.scrollTo({
                    top: offsetPosition,
                    behavior: 'smooth'
                });
            }, 350);
            }
        });
    });

    // Ensure at least one accordion is always open (optional)
    if (oneAccAlwaysOpen === true) {
    const accordions = document.querySelectorAll(".accordion-collapse");
    let opening = false;
    accordions.forEach(function (el) {
      el.addEventListener("hide.bs.collapse", (event) => {
        if (!opening) {
          event.preventDefault();
          event.stopPropagation();
        } else {
          opening = false;
        }
      });
      el.addEventListener("show.bs.collapse", (event) => {
        opening = true;
      });
    });
    }

});
Share:

Comments

Sign in to join the conversation.

No comments yet. Be the first!

More from Tips & Tricks