How to animate multiple framer-motion elements once in viewport

5.1k Views Asked by At

I'm using framer-motion to animate multiple elements in one page. Since framer-motion doesn't have an easy way of animating an element once it's in viewport I'm using this method:

const controls = useAnimation();
const { ref, inView } = useInView();

useEffect(() => {
    if (inView) {
      controls.start("visible");
    }
    if (!inView) {
      controls.start("hidden");
    }
  }, [controls, inView]);

const fadeFromBottom = {
    hidden: {
      opacity: 0,
      y: -5,
    },
    visible: {
      opacity: 1,
      y: 0,
      transition: {
        type: "spring",
        delay: 0.4,
      },
    },
  };

return (
<motion.section
  ref={ref}
  variants={fadeFromBottom}
  initial='hidden'
  animate={controls}
>
   <img src={image} alt='image'>
</motion.section>

However, this method is only allowing me to animate one element per .jsx file. If I wanted to animate two sections with different animations when they enter the viewport, how do I do that? For example how would I animate these two sections at different times with different animations?

<motion.section
  ref={ref}
  variants={fadeFromBottom}
  initial='hidden'
  animate={controls}
>
   <img src={image} alt='image'>
</motion.section>
<motion.section
  ref={ref}
  variants={fadeFromLeft}
  initial='hidden'
  animate={controls}
>
   <img src={image} alt='image'>
</motion.section>
3

There are 3 best solutions below

2
On

You can create a hook that wraps the logic of animating components on "in view"

const useAnimateOnInView = () => {
    const controls = useAnimation();
    const { ref, inView } = useInView();
    
    useEffect(() => {
        if (inView) {
          controls.start("visible");
        }
        if (!inView) {
          controls.start("hidden");
        }
      }, [controls, inView]);
    

     return { ref, controls };
}

The use the hook for all things you want to animate

const { ref: bananaRef, controls: bananaControl } = useAnimateOnInView();
const { ref: appleRef, controls: appleControl } = useAnimateOnInView();

and then hook up the refs to the related dom elements.

<motion.section
  ref={bananaRef}
  variants={fadeFromBottom}
  initial='hidden'
  animate={bananaControl}
>
   <img src={image} alt='banana'>
</motion.section>
<motion.section
  ref={appleRef}
  variants={fadeFromLeft}
  initial='hidden'
  animate={appleControl}
>
   <img src={image} alt='apple'>
</motion.section>

you could also just duplicate the existing use of useInView hook and add some logic to the useEffect. I think this hook cleans it up a bit though.

0
On

I ran into this problem, and this is how I solved it for anyone else who stumbles upon this.

We can still use react-intersection-observer expect instead of importing the useInView() hook, we can import the InView Component

import { InView } from "react-intersection-observer";

Then we can wrap each element with that component

<InView as="div">
        <Image
            src={designPic}
            alt=""
            quality="90"
            loading="lazy"
            objectFit="cover"
            width={718}
            height={649}
            placeholder="blur"
        />
</InView>

We can then create a useAnimation hook control for each element.

const imageOneControls = useAnimation();
const imageTwoControls = useAnimation();
const imageThreeControls = useAnimation();

Then reference the control in each element on using an onChange on the component.

onChange={(inView) => {
    if (inView) imageOneControls.start("show");
    }}

Im not sure if this is the best solution but it was the only one i could come up with when dealing with this issue. The final element wrapped in the component would look something like this:

<InView 
    as="div"
    onChange={(inView) => {
    if (inView) imageOneControls.start("show");
    }}
    >
        <Image
            src={designPic}
            alt=""
            quality="90"
            loading="lazy"
            objectFit="cover"
            width={718}
            height={649}
            placeholder="blur"
        />
</InView>
0
On

I don't know if this has been added to framer motion recently but, here's how I'm doing it so easily: Just add the animation you wanna apply to the element in the whileInView not the animate like this:

<motion.div
    initial={{ opacity: 0 }}
    whileInView={{ opacity: 1 }}
    viewport={{ once: true }}
>
    Your Content
</motion.div>

this viewport={{ once: true }} tells framer motion that you want this animation to happen only one time, not every time the element leaves and enters the view.