Reputation: 293
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
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
.
.hide
means the height
is 0
.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
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