Search code examples
javascriptreactjsnext.jsframer-motion

How to perform a callback after the animation is complete using frame-motion


I have component called AnimatedText. I want the the component to be underlined with the words I filled in the underlineLines prop when the animation is complete. So how do I do that?

I have tried onAnimationEnd function but it didn't bring any change

"use client";
import { motion, useInView, Variant, useAnimationControls } from "framer-motion";
import { useEffect, useRef, useState } from "react";

type AnimatedTextProps = {
    text: string | string[];
    el?: keyof JSX.IntrinsicElements;
    className?: string;
    once?: boolean;
    repeatDelay?: number;
    underlineLines?: number[];
    animation?: {
        hidden: Variant;
        visible: Variant;
    };
};

const defaultAnimations = {
    hidden: {
        opacity: 0,
        y: 5,
    },
    visible: {
        opacity: 1,
        y: 0,
        transition: {
            duration: 0.05,
            ease: "easeOut",
        },
    },
};

const AnimatedText = ({ text, el: Wrapper = "p", className, once, repeatDelay, underlineLines, animation = defaultAnimations }: AnimatedTextProps) => {
    const textArray = Array.isArray(text) ? text : [text];
    const controls = useAnimationControls();
    const ref = useRef(null);
    const isInView = useInView(ref, { amount: 0.1, once });
    const [isAnimationEnd, setIsAnimationEnd] = useState(false);

    useEffect(() => {
        let timeout: NodeJS.Timeout;
        const show = () => {
            controls.start("visible");
            if (repeatDelay) {
                timeout = setTimeout(async () => {
                    await controls.start("hidden");
                    controls.start("visible");
                }, repeatDelay);
            }
        };
        isInView ? show() : controls.start("hidden");

        return () => clearTimeout(timeout);
    }, [isInView]);

    return (
        <Wrapper className={className}>
            <span className="sr-only">{textArray.join(" ")}</span>
            <motion.span
                ref={ref}
                initial="hidden"
                animate={controls}
                variants={{
                    visible: { transition: { staggerChildren: 0.05 } },
                    hidden: {},
                }}
                aria-hidden
            >
                {textArray.map((line, lineIndex) => {
                    return (
                        <span className="block" key={`${line}-${lineIndex}`}>
                            {line.split(" ").map((word, wordIndex) => (
                                <span
                                    className={
                                        underlineLines?.includes(wordIndex) && isAnimationEnd
                                            ? "inline-block q-underline q-underline-black q-underline-start"
                                            : "inline-block q-underline q-underline-black"
                                    }
                                    key={`${word}-${wordIndex}`}
                                >
                                    {word.split("").map((char, charIndex) => (
                                        <motion.span
                                            key={`${char}-${charIndex}`}
                                            className={"inline-block"}
                                            variants={animation}
                                            onAnimationEnd={() => {
                                                setIsAnimationEnd(true);
                                            }}
                                        >
                                            {char}
                                        </motion.span>
                                    ))}
                                    <span className="inline-block">&nbsp;</span>
                                </span>
                            ))}
                        </span>
                    );
                })}
            </motion.span>
        </Wrapper>
    );
};

export default AnimatedText;

Please help me, I have searched for the answer from many discord servers :(


Solution

  • Are you looking for the onAnimationComplete callback on <motion.span>?

    https://www.framer.com/motion/component/#%23%23onanimationcomplete