Accordion that allows only one open at a time

I have an accordion that works really well, it looks good on the site and works as it should. However, I'm trying to add some more JavaScript functionality to it, to make it more it look more professional.

Currently, the accordion allows you to have multiple panels open at one time i.e. if I open one tab, and then open another tab, both tabs will be open at the same time. And the only way to close these panels, is to re-click on the header.

What I would like is some JavaScript code that prevents multiple tabs from being open at one time, so if I click on a new panel, it should close the existing open panel first. Here is my HTML code for the accordion:

var acc = document.getElementsByClassName("accordion");
var i;
for (i = 0; i < acc.length; i++) {
  acc[i].addEventListener("click", function() {
    this.classList.toggle("active");
    var panel = this.nextElementSibling;
    if (panel.style.maxHeight) {
      panel.style.maxHeight = null;
    } else {
      panel.style.maxHeight = panel.scrollHeight + "px";
    }
  });
}
<div class="accordion"><b>Heading 1</b></div>
<div class="panel">
  <p class="text-light">Text 1</p>
</div>
<div class="accordion"><b>Heading 2</b></div>
<div class="panel">
  <p class="text-light">Text 2</p>
</div>

4 answers

  • answered 2021-09-11 18:31 emreozgun10

    1. dynamically add data-id to each accordion div (0 based data-ids)
    2. when clicked -> if the index matches that data-id -> open (display) else (display:none)
    3. I would also add an isActive flag variable : let currentOpenID = cur; so that if you click on an already open accordion element you display:none that too...

    the logic is: you need something to compare against, that would be your open flag and else close.

    I would also use

    panel.style.display: none;
    panel.style.display: block;
    

  • answered 2021-09-11 18:41 Dan Zuzevich

    All you need to do is add a variable that tracks the currently active element. On each click, your code will change the max height of the currently active item to 0. Once it does that, it will set the max height on the item just clicked, and set that item to the currently active one.

    I've rewritten your code a tab bit, but this should definitely work for you.

    var acc = document.querySelectorAll(".accordion");
    var active = null;
    
    acc.forEach((item, i) => {
      item.addEventListener("click", function () {
        this.classList.toggle("active");
        var panel = this.nextElementSibling;
        
        if(active) {
          active.style.maxHeight = null;
        }
        
        if(panel !== active) {
          panel.style.maxHeight = panel.scrollHeight + "px";
          active = panel
        } else {
          active = null
        }
      });
    });
    .panel {
      max-height: 0;
      overflow: hidden;
    }
    <div class="accordion"><b>Heading 1</b></div>
    <div class="panel">
       <p class="text-light">Text 1</p>
    </div>
    <div class="accordion"><b>Heading 2</b></div>
    <div class="panel">
       <p class="text-light">Text 2</p>
    </div>
    <div class="accordion"><b>Heading 3</b></div>
    <div class="panel">
       <p class="text-light">Text 3</p>
    </div>
    <div class="accordion"><b>Heading 4</b></div>
    <div class="panel">
       <p class="text-light">Text 4</p>
    </div>

  • answered 2021-09-11 18:50 kol

    Every time you click a heading you need to not only toggle the state of the clicked item, but also loop over the other accordion items and close them. In the code below I simplified your example a bit and used display: none; to hide accordion items:

    const accordions = Array.from(document.getElementsByClassName("accordion"));
    accordions.forEach(accordion1 =>
      accordion1.addEventListener("click", () =>
        accordions.forEach(accordion2 =>
          accordion2.nextElementSibling.classList.toggle(
            "hidden",
            accordion1 !== accordion2 ||
            !accordion1.nextElementSibling.classList.contains("hidden")
          )
        )
      )
    );
    .panel.hidden {
      display: none;
    }
    <div class="accordion"><b>Heading 1</b></div>
    <div class="panel hidden">Panel 1</div>
    <div class="accordion"><b>Heading 2</b></div>
    <div class="panel hidden">Panel 2</div>
    <div class="accordion"><b>Heading 3</b></div>
    <div class="panel hidden">Panel 3</div>

  • answered 2021-09-11 19:12 Big_Boulard

    A few things I've learned quite recently indeed... One of the best practice when you need to listen to an event on several nodes having the same parent is to use event delegation, that is to say, listen for click on the parent node.

    Then you may need to interact with the other links wrapped in

  • . To handle this, the closest method will help you selecting a common parent ( see https://developer.mozilla.org/en-US/docs/Web/API/Element/closest ) and then select the nodes by using querySelectorAll (or getElementsByClassName if you prefer)

    You'll still have to manage you animation from here but I think it's gonna help you.

    // DOM here
    let nav = document.querySelector(".nav");
    
    // Handlers here
    const clickHandler = function (e) {
      if (e.target.classList.contains("nav__link")) {
        const link = e.target;
        const siblings = link.closest(".nav").querySelectorAll(".nav__link");
    
        link.classList.toggle("active");
    
        // removes all actives except for the clicked one
        siblings.forEach((el) => {
          if (el !== link) el.classList.remove("active");
        });
      }
    };
    
    // Listeners here
    nav.addEventListener("click", clickHandler);
    body {
      font-family: sans-serif;
    }
    
    .nav__link {
      display: block;
      width: 100%;
    }
    .active {
      background: #0f0;
    }
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="UTF-8" />
      </head>
    
      <body>
        <div class="nav">
          <ul class="nav__links">
                    <li class="nav__item">
                        <a class="nav__link" href="#section--1">Section 1</a>
                    </li>
                    <li class="nav__item">
                        <a class="nav__link" href="#section--2">Section 2</a>
                    </li>
                    <li class="nav__item">
                        <a class="nav__link" href="#section--3">Section 3</a>
                    </li>
                    <li class="nav__item">
                        <a class="nav__link" href="#section--4"
                            >Section 4</a
                        >
                    </li>
                </ul>
          </div>
        </div>
    
        <script src="src/index.js"></script>
      </body>
    </html>

    and if you want to get it a step further then it would look like this:

    // DOM here
    let nav = document.querySelector(".nav");
    
    // Handlers here
    const clickHandler = function (e) {
      if (e.target.classList.contains("nav__link")) {
        const link = e.target; // clicked link
    
        const siblings = link.closest(".nav").querySelectorAll(".nav__link");
    
        link.classList.toggle("active");
        link.children[0].classList.toggle("hidden");
    
        // removes all actives except for the clicked one
        siblings.forEach((el) => {
          if (el !== link) {
            el.classList.remove("active");
            el.children[0].classList.add("hidden");
          }
        });
      }
    };
    
    // Listeners here
    nav.addEventListener("click", clickHandler);
    body {
      font-family: sans-serif;
    }
    
    .nav__link {
      display: block;
      width: 100%;
      transition: all 0.3s;
    }
    .active {
      background: #0f0;
    }
    
    .hidden {
      display: none;
    }
        <div class="nav">
          <ul class="nav__links">
                    <li class="nav__item">
                        <a class="nav__link" href="#section--1">Section 1
                            <ul class="hidden">
                                <li>list item</li>
                                <li>list item</li>
                            </ul>
                        </a>
                    </li>
                    <li class="nav__item">
                        <a class="nav__link" href="#section--2">Section 2
                                <ul class="hidden">
                                        <li>list item</li>
                                        <li>list item</li>
                                    </ul>
                        </a> 
                    </li>
                    <li class="nav__item">
                        <a class="nav__link" href="#section--3">Section 3
                                <ul class="hidden">
                                        <li>list item</li>
                                        <li>list item</li>
                                    </ul>
                        </a>
                    </li>
                    <li class="nav__item">
                        <a class="nav__link" href="#section--4">section 4
                            <ul class="hidden">
                                    <li>list item</li>
                                    <li>list item</li>
                                </ul>
                                </a>
                    </li>
                </ul>
          </div>
        </div>

How many English words
do you know?
Test your English vocabulary size, and measure
how many words do you know
Online Test
Powered by Examplum