Business
Jobs
  • About Us
  • Solutions
    • Job Postings
      Post your job and receive qualified candidates in 48h.
    • Candidate Assessments
      500+ technical and psychological tests, plus anti-fraud.
    • Headhunting
      Tailor-made executive search from start to finish.
    • Payroll + EOR
      Payroll dispersal and EOR across 15+ LATAM countries.
  • Pricing
  • Jobs

0

167
Views
Toggling between classes keeps animation from going through, just skips to end

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 class="mobile-menu menu-closed">
  <div class="line"></div>
</div>
about 4 years ago · Juan Pablo Isaza
1 answers
Answer question

0

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 class="mobile-menu">
  <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...

about 4 years ago · Juan Pablo Isaza Report
Answer question
Find remote jobs

Discover the new way to find a job!

Top jobs
Top job categories
Business
Post vacancy Pricing Sales
Legal
Terms and conditions Privacy policy
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Show me some job opportunities
There's an error!