Search code examples
javascriptnavigationtoggle

Vanilla Javascript Toggle Drop Down Menu


My brain has checked out for weekend...

I am looking for a solution in plain Javascript where if one dropdown menu box is opened on click of another main menu item, the previous opened dropdown would close and then display the newly clicked main menu item's dropdown. I know this is probably so simple, but I cannot come up with a solution that is not convoluted.

Also if you click outside of the menu items (anywhere on the document that is not a menu item or dropdown box) should close any open dropdowns.

Thank you for any help.

function testFunc(el) {
  var parent = el.parentElement;
  var dd = parent.lastChild.previousSibling;
  dd.classList.toggle('show');
}
ul { list-style: none; margin: 0; padding: 0; }
ul li {
  width: 100px;
  float: left;
  background: #dbdbdb;
  line-height: 2em;
  text-align: center;
  margin: 0 5px;
  cursor: pointer;
}
ul li span {
  display: block;
}
ul li ul {
  display: none;
}

.show {
  display: block;
}
<ul>
  <li>
    <span onclick="testFunc(this)">Item 1</span>
    <ul>
      <li>Sub Item 1</li>
      <li>Sub Item 2</li>
    </ul>
  </li>
  <li>
    <span onclick="testFunc(this)">Item 2</span>
    <ul>
      <li>Sub Item 1</li>
      <li>Sub Item 2</li>
    </ul>
  </li>
  <li>
    <span onclick="testFunc(this)">Item 3</span>
    <ul>
      <li>Sub Item 1</li>
      <li>Sub Item 2</li>
    </ul>
  </li>
  <li>
    <span onclick="testFunc(this)">Item 4</span>
    <ul>
      <li>Sub Item 1</li>
      <li>Sub Item 2</li>
    </ul>
  </li>
</ul>


Solution

  • Toggling menu visibility

    You can save the last opened menu in a variable opened outside the function. Then when a menu is clicked if opened is not null it will toggle the opened (i.e. hide the last opened menu) and toggle the clicked item.

    let opened = null
    
    function testFunc(el) {
    
      // gets the <ul> element of the clicked menu item
      const menu = el.parentElement.lastChild.previousSibling;
    
      if (!opened) {
    
        // no menu item is shown
        opened = menu
        opened.classList.toggle('show');
    
      } else if (menu == opened) {
    
        // the clicked item is already showing
        menu.classList.toggle('show')
        opened = null
    
      } else {
    
        // the clicked item is hiddden but another one is showing
        opened.classList.toggle('show')
        opened = menu
        opened.classList.toggle('show')
    
      }
    
    }
    

    Here is the code:

    let opened = null
    
    function testFunc(el) {
    
      const menu =  el.parentElement.lastChild.previousSibling;
      
      if(!opened) {
        opened = menu
        opened.classList.toggle('show');
      } else if(menu == opened) {
        menu.classList.toggle('show')
        opened = null
      } else {
        opened.classList.toggle('show')
        opened = menu
        opened.classList.toggle('show')
      }
      
    }
    ul {
      list-style: none;
      margin: 0;
      padding: 0;
    }
    
    ul li {
      width: 100px;
      float: left;
      background: #dbdbdb;
      line-height: 2em;
      text-align: center;
      margin: 0 5px;
      cursor: pointer;
    }
    
    ul li span {
      display: block;
    }
    
    ul li ul {
      display: none;
    }
    
    .show {
      display: block;
    }
    <ul>
      <li>
        <span onclick="testFunc(this)">Item 1</span>
        <ul>
          <li>Sub Item 1</li>
          <li>Sub Item 2</li>
        </ul>
      </li>
      <li>
        <span onclick="testFunc(this)">Item 2</span>
        <ul>
          <li>Sub Item 1</li>
          <li>Sub Item 2</li>
        </ul>
      </li>
      <li>
        <span onclick="testFunc(this)">Item 3</span>
        <ul>
          <li>Sub Item 1</li>
          <li>Sub Item 2</li>
        </ul>
      </li>
      <li>
        <span onclick="testFunc(this)">Item 4</span>
        <ul>
          <li>Sub Item 1</li>
          <li>Sub Item 2</li>
        </ul>
      </li>
    </ul>


    A variant with ES6 syntax

    Here is a variant with some ES6 syntax, note I have changed the HTML naming structure to better maintain the code, calling the elements by class name allows

    • to not have to use of inline event listeners

    • call all the menu items in one line

    Here is the JavaScript code:

    let opened = null
    const toggleVisibility = e => e.classList.toggle('show')
    
    const toggleDropDown = e => {
    
      const clickedItem = e.target.parentElement.lastChild.previousSibling
    
      toggleVisibility(clickedItem);
    
      if (!opened) {
        opened = clickedItem
      } else if (opened == clickedItem) {
        opened = null
      } else {
        toggleVisibility(opened);
        opened = clickedItem
      }
    
    }
    
    [...document.querySelectorAll('.dropDown')].forEach(dropDown => dropDown.addEventListener('click', toggleDropDown))
    

    let opened = null
    const toggleVisibility = e => e.classList.toggle('show')
    
    const toggleDropDown = e => {
    
      const clickedItem = e.target.parentElement.lastChild.previousSibling
    
      toggleVisibility(clickedItem);
    
      if (!opened) {
        opened = clickedItem
      } else if (opened == clickedItem) {
        opened = null
      } else {
        toggleVisibility(opened);
        opened = clickedItem
      }
    
    }
    
    [...document.querySelectorAll('.dropDown')].forEach(dropDown => dropDown.addEventListener('click', toggleDropDown))
    ul {
      list-style: none;
      margin: 0;
      padding: 0;
    }
    
    ul li {
      width: 100px;
      float: left;
      background: #dbdbdb;
      line-height: 2em;
      text-align: center;
      margin: 0 5px;
      cursor: pointer;
    }
    
    ul li span {
      display: block;
    }
    
    ul li ul {
      display: none;
    }
    
    .show {
      display: block;
    }
    <ul>
      <li>
        <span class="dropDown">Item 1</span>
        <ul>
          <li>Sub Item 1</li>
          <li>Sub Item 2</li>
        </ul>
      </li>
      <li>
        <span class="dropDown">Item 2</span>
        <ul>
          <li>Sub Item 1</li>
          <li>Sub Item 2</li>
        </ul>
      </li>
      <li>
        <span class="dropDown">Item 3</span>
        <ul>
          <li>Sub Item 1</li>
          <li>Sub Item 2</li>
        </ul>
      </li>
      <li>
        <span class="dropDown">Item 4</span>
        <ul>
          <li>Sub Item 1</li>
          <li>Sub Item 2</li>
        </ul>
      </li>
    </ul>


    Toggling menu visibility + closing when clicking elsewhere

    If you want to close any opened menu if the user clicks outside of the menu you'll need to have an event listener on the document itself. So instead of having one event listener per menu button, you will have a single one watching for any click happening in the document.

    The event listener will determine if the clicked item is a menu button, in this case, it will run the menu handler. Else it will close the last opened menu item.

    JavaScript code:

    let opened = null
    const toggleVisibility = e => e.classList.toggle('show')
    
    const handleDropdown = e => {
    
      const clickedItem = e.parentElement.lastChild.previousSibling
    
      toggleVisibility(clickedItem)
    
      if (!opened) {
        opened = clickedItem
      } else if (opened == clickedItem) {
        opened = null
      } else {
        toggleVisibility(opened)
        opened = clickedItem
      }
    
    }
    
    const handleClick = e => {
    
      if (e.target.className.includes('dropDown')) {
        handleDropdown(e.target)
      } else if (opened) {
        toggleVisibility(opened)
        opened = null
      }
    
    }
    
    document.addEventListener('click', handleClick)
    

    Here is the full code:

    let opened = null
    const toggleVisibility = e => e.classList.toggle('show')
    
    const handleDropdown = e => {
    
      const clickedItem = e.parentElement.lastChild.previousSibling
    
      toggleVisibility(clickedItem)
    
      if (!opened) {
        opened = clickedItem
      } else if (opened == clickedItem) {
        opened = null
      } else {
        toggleVisibility(opened)
        opened = clickedItem
      }
    
    }
    
    const handleClick = e => {
    
      if (e.target.className.includes('dropDown')) {
        handleDropdown(e.target)
      } else if (opened) {
        toggleVisibility(opened)
        opened = null
      }
    
    }
    
    document.addEventListener('click', handleClick)
    ul {
      list-style: none;
      margin: 0;
      padding: 0;
    }
    
    ul li {
      width: 100px;
      float: left;
      background: #dbdbdb;
      line-height: 2em;
      text-align: center;
      margin: 0 5px;
      cursor: pointer;
    }
    
    ul li span {
      display: block;
    }
    
    ul li ul {
      display: none;
    }
    
    .show {
      display: block;
    }
    <ul>
      <li>
        <span class="dropDown">Item 1</span>
        <ul>
          <li>Sub Item 1</li>
          <li>Sub Item 2</li>
        </ul>
      </li>
      <li>
        <span class="dropDown">Item 2</span>
        <ul>
          <li>Sub Item 1</li>
          <li>Sub Item 2</li>
        </ul>
      </li>
      <li>
        <span class="dropDown">Item 3</span>
        <ul>
          <li>Sub Item 1</li>
          <li>Sub Item 2</li>
        </ul>
      </li>
      <li>
        <span class="dropDown">Item 4</span>
        <ul>
          <li>Sub Item 1</li>
          <li>Sub Item 2</li>
        </ul>
      </li>
    </ul>