skip to Main Content

I’m trying to make a CSS animation that bounces back and forth, with a pause at each end. Using the Javascript method element.animate() seems to do the trick, except that using keyframes breaks the easing function, which doesn’t happen when using only CSS.

Am I using animate() wrong? Is it a bug? Why do Javascript and CSS behave differently? The animate MDN docs don’t say anything about this.

const element = document.getElementById("animation")
const anim = element.animate(
  [
    { offset: 0, transform: "translateX(0)" },
    { offset: .1, transform: "translateX(0)" },
    { offset: .9, transform: "translateX(50vw)" },
    { offset: 1, transform: "translateX(50vw)" },
  ],
  {
    duration: 5000,
    easing: "ease-in-out",
    direction: "alternate",
    iterations: Infinity,
  }
);
<div id="animation">Ball</div>

Creating the same animation in CSS works with the correct easing.

const element = document.getElementById("animationJS")
const anim = element.animate(
  [
    { offset: 0, transform: "translateX(0)" },
    { offset: .1, transform: "translateX(0)" },
    { offset: .9, transform: "translateX(50vw)" },
    { offset: 1, transform: "translateX(50vw)" },
  ],
  {
    duration: 5000,
    easing: "ease-in-out",
    direction: "alternate",
    iterations: Infinity,
  }
);
#animationCSS {
    animation-name: move;
    animation-direction: alternate;
    animation-duration: 5s;
    animation-timing-function: ease-in-out;
    animation-iteration-count: infinite;
}

@keyframes move {
    from {
      transform: translateX(0);
    }
    10% {
        transform: translateX(0);
    }
    90% {
        transform: translateX(50vw);
    }
    to {
        transform: translateX(50vw);
    }
}
<div id="animationJS">Ball1</div>
<div id="animationCSS">Ball2</div>

However, I can’t use CSS because I want the keyframe percentages/offsets to be changed programmatically.

Also, removing the middle two keyframes in the JS code fixes it, but it still isn’t what I want.

2

Answers


  1. Chosen as BEST ANSWER

    I've found the solution, the corresponding JS code that works the same as the CSS one.

    const element = document.getElementById("animationJS")
    const anim = element.animate(
      [
        { offset: 0, transform: "translateX(0)" },
        { offset: .1, transform: "translateX(0)", easing: "ease-in-out" },
        { offset: .9, transform: "translateX(50vw)"},
        { offset: 1, transform: "translateX(50vw)" },
      ],
      {
        duration: 5000,
        direction: "alternate",
        iterations: Infinity,
      }
    );
    #animationCSS {
        animation-name: move;
        animation-direction: alternate;
        animation-duration: 5s;
        animation-timing-function: ease-in-out;
        animation-iteration-count: infinite;
    }
    
    @keyframes move {
        from {
          transform: translateX(0);
        }
        10% {
            transform: translateX(0);
        }
        90% {
            transform: translateX(50vw);
        }
        to {
            transform: translateX(50vw);
        }
    }
    <div id="animationJS">Ball1</div>
    <div id="animationCSS">Ball2</div>

    Adding the easing function only to the middle part of the animation and removing it from the whole does the trick.

    I have no idea why it works like this, and I can't find this specific difference in the documentation, but at least it works.


  2. If you remove any of the confusing aspects of the animation (as the easing goes over your whole value range, so it starts at 0 and ends at 1, which means you are missing the first 10% of the ease-in and the last 10% of the ease-out part), and make your values a little bit bigger to really see the easing in action, you will notice that it currently works as expected. It’s easy to get confused by animations, definitely subtle ones.

    const element = document.getElementById("animation")
    const anim = element.animate(
      [
        { offset: 0, transform: "translateX(0)" },
        { offset: 1, transform: "translateX(100vw) translateX(-100%)" },
      ],
      {
        duration: 5000,
        easing: "ease-in-out",
        direction: "alternate",
        iterations: Infinity,
      }
    );
    #animation { position: absolute; left: 0; top: 0; }
    <div id="animation">Ball</div>

    You can see this in the above snippet: speed up, slow down: ease-in-out is working as expected. I can see what you are trying to accomplish though, having a 20% break between animation timelines using a single timeline, but as far as I understand, the easing applies across your whole timeline. So due to that you might want to set up and infinite loop in order to add a delay:

    async function delay( ms ){
        
      return new Promise(r => setTimeout(r, ms));
      
    }
    async function loopAnimation(){
      
      const element = document.getElementById("animation")
    
      let direction = 'normal';
      let animation;
    
      while( true ){
        
        await delay(1000);
        
        // Make sure previous an animations are removed and don't stack up
        if( animation ) animation.cancel();
        
        animation = element.animate([
          { offset: 0, transform: "translateX(0)" },
          { offset: 1, transform: "translateX(100vw) translateX(-100%)" },
        ],{
          duration: 3000,
          easing: "ease-in-out",
          direction: direction,
          iterations: 1,
          fill: 'both'
        })
      
        await animation.finished;
        await delay(1000);
        
        // Reverse the animation
        direction = direction === 'normal' ? 'reverse' : 'normal';
    
      }
    
    }
    
    loopAnimation();
    #animation { position: absolute; left: 0; top: 0; }
    <div id="animation">Ball</div>

    Let’s merge your examples

    As you can see below, the animations are 99% the same, I suppose the difference is just a slight difference in timing with JS and loading, but they run almost exactly the same (I did have to correct your animation in JS so it had .9 and not .8 as the offset):

    const element = document.getElementById("animate-js")
    const anim = element.animate(
      [
        { offset: 0, transform: "translateX(0)" },
        { offset: .1, transform: "translateX(0)" },
        { offset: .9, transform: "translateX(10vw)" },
        { offset: 1, transform: "translateX(10vw)" },
      ],
      {
        duration: 5000,
        easing: "ease-in-out",
        direction: "alternate",
        iterations: Infinity,
      }
    );
    #animate-css {
      animation-name: move;
      animation-direction: alternate;
      animation-duration: 5s;
      animation-timing-function: ease-in-out;
      animation-iteration-count: infinite;
      color: red;
    }
    
    @keyframes move {
      from {
        transform: translateX(0);
      }
      10% {
        transform: translateX(0);
      }
      90% {
        transform: translateX(10vw);
      }
      to {
        transform: translateX(10vw);
      }
    }
    <div id="animate-js">Animate</div>
    <div id="animate-css">Animate</div>

    Actually, no, the weirdness is still because you trying to add a delay in the loop:

    const element = document.getElementById("animate-js")
    const anim = element.animate(
      [
        { offset: 0, transform: "translateX(0)" },
        { offset: 1, transform: "translateX(10vw)" },
      ],
      {
        duration: 5000,
        easing: "ease-in-out",
        direction: "alternate",
        iterations: Infinity,
      }
    );
    #animate-css {
      animation-name: move;
      animation-direction: alternate;
      animation-duration: 5s;
      animation-timing-function: ease-in-out;
      animation-iteration-count: infinite;
      color: red;
    }
    
    @keyframes move {
      from {
        transform: translateX(0);
      }
      to {
        transform: translateX(10vw);
      }
    }
    <div id="animate-js">Animate</div>
    <div id="animate-css">Animate</div>

    Without that, they are perfectly in sync.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search