Search code examples
next.jstailwind-cssframer-motion

How can I safely loop this framer-motion useAnimate animation?


I made this animation with framer-motion using the useAnimate hook because I thought it would be appropiate. It's basically an infinite word carousel. But I have no idea how to loop the animation infinitely.

import { useAnimate, cubicBezier } from "framer-motion";
import Image from "next/image";
import highlight from "@/public/highlight.svg";
import { useEffect, useRef } from "react";

const Heading = () => {
  const [scope, animate] = useAnimate();
  const isMounted = useRef(true); 


  const wordsHorizontal = ["Automate", "Enhance", "Streamline", "Automate"];
  const wordsVertical = [
    "Efficiency",
    "Productivity",
    "Performance",
    "Efficiency",
  ];

  useEffect(() => {
    const handleAnim = async () => {
        await animate(scope.current, { x: -525 }, { duration: 0.6, ease: cubicBezier(0.77,0,0.18,1), delay: 1});
        await animate(scope.current, { x: -1050 }, { duration: 0.6, ease: cubicBezier(0.77,0,0.18,1), delay: 1});
        await animate(scope.current, { x: -1575 }, { duration: 0.6, ease: cubicBezier(0.77,0,0.18,1), delay: 1});
        await animate(scope.current, { x: 0 }, { duration: 0 });
    };
    handleAnim()
  }, []);

  return (
    <div className="flex flex-col space-y-8">
      <div className="flex space-x-6 items-start">
        <div className="relative text-display font-bold w-[525px] h-[120px]">
          <div className="absolute inset-0 bg-g-red rounded-3.5xl flex items-center overflow-hidden">
            <div ref={scope} className="w-[2100px] flex">
              {wordsHorizontal.map((word, index) => (
                <div
                  key={index}
                  className="min-w-[525px] rounded-3.5xl flex items-center justify-center"
                >
                  {word}
                </div>
              ))}
            </div>
...

I tried making the function call itself, or using an infinite for loop, while loop... etc.


Solution

  • Turn handleAnim into a recursive function (a function that calls itself) and have it call itself after the last animation finishes. Framer Motion gives us a couple examples on how to run a function when an animation completes here. Here's how you can have your animation run infinitely:

    useEffect(() => {
        const handleAnim = async () => {
            await animate(scope.current, { x: -525 }, { duration: 0.6, ease: cubicBezier(0.77,0,0.18,1), delay: 1});
            await animate(scope.current, { x: -1050 }, { duration: 0.6, ease: cubicBezier(0.77,0,0.18,1), delay: 1});
            await animate(scope.current, { x: -1575 }, { duration: 0.6, ease: cubicBezier(0.77,0,0.18,1), delay: 1});
            await animate(scope.current, { x: 0 }, { duration: 0 });
            // Run the animation function again
            handleAnim()
        };
        handleAnim()
      }, []);
    

    Here is a working example. You can also read more about recursion in the MDN docs.