I'm trying to animate a hamburger menu by having the bottom and top line translate to the middle and then rotate into an X and want to reverse the animation when the X is clicked. Using jquery I'm toggling the class menu-open and menu-closed. When I remove the CSS for the menu-closed animation, it fires just fine but when I add the CSS back the animations just skip to the last frame. It forms what I want but just refuses to use the animation fully.
CSS
.navbar .mobile-menu.menu-open .line::before {
animation: menu-open-top 250ms linear forwards;
}
.navbar .mobile-menu.menu-open .line {
animation: menu-middle 250ms linear forwards;
}
.navbar .mobile-menu.menu-open .line::after {
animation: menu-open-bottom 250ms linear forwards;
}
.navbar .mobile-menu.menu-closed .line::before {
animation: menu-open-top 250ms linear reverse;
}
.navbar .mobile-menu.menu-closed .line {
animation: menu-middle 250ms linear reverse;
}
.navbar .mobile-menu.menu-closed .line::after {
animation: menu-open-bottom 250ms linear reverse;
}
Animation
@keyframes menu-open-top {
30% {
bottom: 0;
}
60% {
bottom: 0;
transform: rotate(0) translate(0);
}
100% {
transform: rotate(45deg) translate(5px, 5px);
visibility: visible;
}
}
@keyframes menu-middle {
40% {
visibility: hidden;
}
to {
visibility: hidden;
}
}
@keyframes menu-open-bottom {
30% {
top: 0;
}
60% {
top: 0;
transform: rotate(0) translate(0);
}
100% {
transform: rotate(-45deg) translate(6px, -6px);
visibility: visible;
}
}
JS
$(".mobile-menu").click(expandMenu);
function expandMenu() {
$(".primary-nav").toggleClass("menu-expand");
$(this).toggleClass("menu-open menu-closed");
}
I must be missing something or maybe I need to add new keyframes for the reverse animation but that feels like it would be unnecessary.
edit: here is the html as well
HTML
<div >
<div ></div>
</div>
CodePudding user response:
Here's how to do it using simple prop value changes with careful timing. I guess it can be done using @keyframe animations as well, but I find them more difficult to follow/control/sync, at least in this case, considering it's (basically) a two-step animation.
document.querySelector('.mobile-menu').addEventListener('click', ({
target
}) => {
target.closest('.mobile-menu').classList.toggle('menu-open');
})
.mobile-menu {
--duration: 0.42s;
--size: 3rem;
--padding: 0.5rem;
--color: red;
--distance-timing: cubic-bezier(0.5, 0, 0.3, 1);
--rotation-timing: cubic-bezier(0.4, 0, 0.2, 1);
width: var(--size);
height: var(--size);
padding: var(--padding);
display: flex;
flex-direction: column;
justify-content: space-between;
cursor: pointer;
}
.mobile-menu * {
box-sizing: border-box;
}
.mobile-menu>div {
border: 1px solid var(--color);
height: 0;
width: 100%;
position: relative;
transform-origin: 50% 50%;
transition:
top calc(0.6 * var(--duration)) var(--distance-timing) calc(0.4 * var(--duration)),
bottom calc(0.6 * var(--duration)) var(--distance-timing) calc(0.4 * var(--duration)),
transform calc(0.8 * var(--duration)) var(--rotation-timing) 0s;
}
.mobile-menu> :nth-child(1) {
top: calc(var(--padding)/2);
}
.mobile-menu> :nth-child(3) {
bottom: calc(var(--padding)/2);
}
.mobile-menu.menu-open>div {
transition:
top calc(0.4 * var(--duration)) var(--distance-timing) 0s,
bottom calc(0.4 * var(--duration)) var(--distance-timing) 0s,
transform calc(0.8 * var(--duration)) var(--rotation-timing) calc(0.2 * var(--duration));
}
.mobile-menu.menu-open> :nth-child(1) {
top: calc(50% - 1px);
transform: rotate(0.125turn);
}
.mobile-menu.menu-open> :nth-child(2) {
transform: rotate(0.125turn);
}
.mobile-menu.menu-open> :nth-child(3) {
bottom: calc(50% - 1px);
transform: rotate(-0.125turn);
}
<div >
<div></div>
<div></div>
<div></div>
</div>
Same thing, in SCSS: https://jsfiddle.net/websiter/dybre2f9/
I've extracted the values into CSS vars, so it can be reused and modified with ease. Feel free to tweak it to your liking.
Note: the reason why I'm using bottom
and top
to animate the movement (and not translateY
- which is slightly more performant) is because I wanted the two animations to be completely independent of each other, to allow me to play with various overlapping values and timing functions. What I've come up with doesn't respect the requirement 100% (as in, it slightly overlaps the rotation with the movement - but I'm doing it on purpose, as not overlapping them looks too mechanical). When overlapped, the entire animation seems more organic. It's like the button is alive and happy to have been asked to do the animation. Or maybe I'm just a bit crazy, I don't know...