Snorlax
Snorlax

Reputation: 293

How to Animate closing dropdown menu with js and css?

I have a menu with submenu items, i managed to create a simple animation with css keyframe when opening dropdown, but i can't do the same when dropdown is closed. How can I add animation when the dropdown is closed ? As you can see, when you close the dropdown there is no transition, it just disappears instantly.

Old Snippet

var dropdownBtn = document.querySelectorAll('.menu-btn');
//Add this for toggling dropdown
lastOpened = null;

dropdownBtn.forEach(btn => btn.addEventListener('click', function() {
  var menuContent = this.nextElementSibling;
  menuContent.classList.toggle("show"); 
  
  //Add this for toggling dropdown
  if (lastOpened && lastOpened !== menuContent)
      lastOpened.classList.remove("show");
      lastOpened = menuContent;
}));
.menu-btn {
  background: #e0e0e0;
  padding: 10px;
  margin: 5px 0px 0px 0px;
}

.menu-btn:hover {
  background: #000;
  color: #fff;
}


.drop_container {
   display: none;
   background-color: #017575;
   animation:animateFromBottom .3s;
}

.drop_container.show {
  display: block;
  
}

.drop_container > .item {
  display: flex;
  flex-direction: column;
  margin-left: 10px;
  padding: 10px 0px 0px 0px;
}

@keyframes animateFromBottom {
    from{bottom:-50px;opacity:0} 
    to{bottom:0;opacity:1}
}

@keyframes animateToBottom {
  from{bottom:0;opacity:1} 
  to{bottom:-50px;opacity:0}
}
<div class="dropdown-menu">

<div class="menu-btn">One</div>
<div class="drop_container">
  <a class="item" href="#">Contact Us</a>
  <a class="item" href="#">Visit Us</a>
</div>

<div class="menu-btn">Two</div>
<div class="drop_container">
  <a class="item" href="#">Contact Us</a>
  <a class="item" href="#">Visit Us</a>
</div>

</div>

Edit Snippet: I decided not to use css keyframe, just transition with max-height. This makes it easier for me to make changes, I'm still just a beginner and just stick to the simple stuff. However, when you switch between items it is still not playing any animation. I see that @EmielZuurbier's solution adds animation even when switching from one item to another, how can I make this to my modified code ?

var dropdownBtn = document.querySelectorAll('.menu-btn');
//Add this for toggling dropdown
lastOpened = null;

dropdownBtn.forEach(btn => btn.addEventListener('click', function() {
  var menuContent = this.nextElementSibling;
  if (!menuContent.classList.contains("show")) {
  menuContent.classList.add("show");
  menuContent.classList.remove("hide");
  } else {
  menuContent.classList.add("hide");
  menuContent.classList.remove("show");
  }
  
  //Add this for toggling dropdown
  if (lastOpened && lastOpened !== menuContent)
      lastOpened.classList.remove("show");
      lastOpened = menuContent;
}));
.menu-btn {
  background: #e0e0e0;
  padding: 10px;
  margin: 5px 0px 0px 0px;
}

.menu-btn:hover {
  background: #000;
  color: #fff;
}

.drop_container {
  overflow: hidden;
  max-height: 0; 
}

.drop_container.show {
  max-height: 300px;
  transition: max-height 0.3s ease-in;
}

.drop_container.hide {
  overflow: hidden;
  max-height: 0;
  transition: max-height 0.3s ease-out;
}

.drop_container > .item {
  display: flex;
  flex-direction: column;
  margin-left: 10px;
  padding: 10px 0px 0px 0px;
}
<div class="dropdown-menu">

<div class="menu-btn">One</div>
<div class="drop_container">
  <a class="item" href="#">Contact Us</a>
  <a class="item" href="#">Visit Us</a>
</div>

<div class="menu-btn">Two</div>
<div class="drop_container">
  <a class="item" href="#">Contact Us</a>
  <a class="item" href="#">Visit Us</a>
</div>

</div>

Upvotes: 3

Views: 9598

Answers (2)

Rounin
Rounin

Reputation: 29493

Here is a different solution which involves CSS Transitions and CSS Custom Properties.

With this approach the accordion menu sections are initially set to .hide.

The sections are then toggled (using .classList.toggle()) between .hide and .show.

  • a class of .hide means the height is 0
  • a class of .show means the height is var(--openHeight)

The height will animate between these two values.

The var(--openHeight) is calculated individually for each menuItemData - it is equivalent to menuItemData.scrollHeight.

Via this technique we can enable a smooth CSS transition between 0 and a value which CSS cannot guess at, but JavaScript can readily tell us.


Working Example:

let dropdownMenuItemTitles = document.querySelectorAll('.dropdown-menu-item-title');

dropdownMenuItemTitles.forEach(menuItemTitle => {
  menuItemTitle.addEventListener('click', (e) => {

    const menuItemData = e.target.nextElementSibling;

    menuItemData.style.setProperty('--openHeight', menuItemData.scrollHeight + 'px');

    menuItemData.classList.toggle('show');
    menuItemData.classList.toggle('hide');
  })
});
.dropdown-menu-item-title {
  background-color: #a0a0a0;
  padding: 10px;
  margin: 5px 0px 0px 0px;
  cursor: pointer;
}

.dropdown-menu-item-title:hover {
  background: #000;
  color: #fff;
}

.dropdown-menu-item-data {
  margin: 0;
  overflow: hidden;
  transition: height 0.3s ease-out;
}

.dropdown-menu-item-data.hide {
  height: 0;
}

.dropdown-menu-item-data.show {
  height: var(--openHeight);
}

.dropdown-submenu {
  padding: 0;
  background-color: #e0e0e0;
  list-style-type: none;
}

.dropdown-submenu-item {
  padding: 12px;
}
<dl class="dropdown-menu">
  <div>
    <dt class="dropdown-menu-item-title">One</dt>
    <dd class="dropdown-menu-item-data hide">
      <ul class="dropdown-submenu">
        <li class="dropdown-submenu-item"><a href="#">Contact Us</a></li>
        <li class="dropdown-submenu-item"><a href="#">Visit Us</a></li>
      </ul>
    </dd>
  </div>
  
  <div>
    <dt class="dropdown-menu-item-title">Two</dt>
    <dd class="dropdown-menu-item-data hide">
      <ul class="dropdown-submenu">
        <li class="dropdown-submenu-item"><a href="#">About Us</a></li>
        <li class="dropdown-submenu-item"><a href="#">Visit Us</a></li>
        <li class="dropdown-submenu-item"><a href="#">Opening Times</a></li>
        <li class="dropdown-submenu-item"><a href="#">Contact Us</a></li>
      </ul>
    </dd>
  </div>

  <div>
    <dt class="dropdown-menu-item-title">Three</dt>
    <dd class="dropdown-menu-item-data hide">
      <ul class="dropdown-submenu">
        <li class="dropdown-submenu-item"><a href="#">About Us</a></li>
        <li class="dropdown-submenu-item"><a href="#">Visit Us</a></li>
        <li class="dropdown-submenu-item"><a href="#">Contact Us</a></li>
      </ul>
    </dd>
  </div>
</dl>

</div>


Further Reading:

Upvotes: 2

Emiel Zuurbier
Emiel Zuurbier

Reputation: 20944

There are multiple ways to achieve this. Because you're showing and hiding the drop down lists with the display property we'll need to stick to animations.

Create a new class for the animateToBottom keyframes. This class should be added after an element with the show class should animate out.

Only after the "out" animation has finished should the show class be removed. With the animationend event we can see when our animation finishes so we can hide the dropdown.

const dropdownBtns = document.querySelectorAll('.menu-btn');
let lastOpened = null;

dropdownBtns.forEach(btn => btn.addEventListener('click', function() {
  const menuContent = this.nextElementSibling;

  if (lastOpened !== null) {
    const target = lastOpened;
 
    target.addEventListener('animationend', () => {
      target.classList.remove('show', 'animate-out');
 
      if (target === lastOpened) {
        lastOpened = null;
      }
    }, {
      once: true
    });

    target.classList.add('animate-out');
  }

  if (lastOpened !== menuContent) {
    menuContent.classList.add('show');
    lastOpened = menuContent;
  }
}));
.menu-btn {
  background: #e0e0e0;
  padding: 10px;
  margin: 5px 0px 0px 0px;
}

.menu-btn:hover {
  background: #000;
  color: #fff;
}

.drop_container {
  display: none;
  background-color: #017575;
  animation: animateFromBottom .3s;
}

.drop_container.show {
  display: block;
}

.drop_container.show.animate-out {
  animation: animateToBottom .3s;
}

.drop_container>.item {
  display: flex;
  flex-direction: column;
  margin-left: 10px;
  padding: 10px 0px 0px 0px;
}

@keyframes animateFromBottom {
  from {
    transform: translate3d(0, 10px, 0);
    opacity: 0
  }
  to {
    transform: translate3d(0, 0, 0);
    opacity: 1
  }
}

@keyframes animateToBottom {
  from {
    transform: translate3d(0, 0, 0);
    opacity: 1
  }
  to {
    transform: translate3d(0, 10px, 0);
    opacity: 0
  }
}
<div class="dropdown-menu">

  <div class="menu-btn">One</div>
  <div class="drop_container">
    <a class="item" href="#">Contact Us</a>
    <a class="item" href="#">Visit Us</a>
  </div>

  <div class="menu-btn">Two</div>
  <div class="drop_container">
    <a class="item" href="#">Contact Us</a>
    <a class="item" href="#">Visit Us</a>
  </div>

</div>

Upvotes: 3

Related Questions