1

While using Framer Motion API to create interaction and animations on my site, I can not find how to use it in order to trigger an animation when something is on the screen.

For example, this SVG draws correctly, but Framer does not wait for the element to be on the viewport and triggers it right after loading site:

import React, { Component } from 'react'
import { motion } from "framer-motion";

class IsometricScreen extends Component {

    constructor() {
        super()
        this.icon = {
            hidden: { pathLength: 0 },
            visible: { pathLength: 1 }
        }
    }

    render() {
        return (
            <motion.svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 1000" className="svg-mobile">
                <motion.path
                    d="M418,988.93H82c-39.76,0-72-32.24-72-72V83.07c0-39.76,32.24-72,72-72h336c39.76,0,72,32.24,72,72v833.86
                    C490,956.69,457.76,988.93,418,988.93z"
                    variants={this.icon}
                    initial="hidden"
                    animate="visible"
                    transition={{
                        default: { duration: 2, ease: "easeInOut" }
                    }}
                />
            </motion.svg>
        )
    }
}

export default IsometricScreen

Does Framer have a viewport detection triggerer to be implemented here?

Biomehanika
  • 1,530
  • 1
  • 17
  • 45
  • Looks like a duplicate of https://stackoverflow.com/questions/58958972/framer-motion-animate-when-element-is-in-view-when-you-scroll-to-element. I provided a solution over there. – amann Feb 20 '20 at 16:36

3 Answers3

19

Alternatively, you can use Intersection Observer, blends pretty well with React and framer motion.

import { useInView } from "react-intersection-observer"; // 1.9K gzipped
import { motion, useAnimation } from "framer-motion";

const Component = () => {
    const animation = useAnimation();    
    const [ref, inView, entry] = useInView({ threshold: 0.1 });

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

    const variants = {
        visible: {
          y: 0,
          opacity: 1,
          transition: { duration: 0.5, delayChilden: 0.2, staggerChildren: 0.1 },
        },
        hidden: {
          y: enter,
          opacity: 0,
        },
    }

    return (
        <motion.div
          ref={ref}
          animate={animation}
          initial="hidden"
          variants={{variants}}
        />
      );
}

You can also refine your animation by looking at entry object (entering from top or bottom, etc)

Jawkel
  • 305
  • 4
  • 8
  • 1
    How are "entry" and "enter" being used? – srWebDev Mar 16 '21 at 20:35
  • This seems to be triggered by the bottom of the div. Meaning that none of it is visible until the entire div has been scrolled into view. How can I get it to fire once the top 10% of the div's height has entered the viewport? In a vanilla js IntersectionObserver() the options would be {root: null, threshold: 0, rootMargin: "-10%",} – srWebDev Mar 16 '21 at 20:50
5

framer-motion has built-in support for this use case since version 5.3.

Here's a CodeSandbox demonstrating the pattern: https://codesandbox.io/s/framer-motion-animate-in-view-5-3-94j13

Relevant code:

function FadeInWhenVisible({ children }) {
  return (
    <motion.div
      initial="hidden"
      whileInView="visible"
      viewport={{ once: true }}
      transition={{ duration: 0.3 }}
      variants={{
        visible: { opacity: 1, scale: 1 },
        hidden: { opacity: 0, scale: 0 }
      }}
    >
      {children}
    </motion.div>
  );
}

Usage:

<FadeInWhenVisible>
  <Box />
</FadeInWhenVisible>
amann
  • 5,449
  • 4
  • 38
  • 46
-1

I have finally solved this with a tiny functional component:

function inViewport() {

    const isInViewport = el => {
        const rect = el.getBoundingClientRect()
        const vertInView = (rect.top <= window.innerHeight) && ((rect.top + rect.height) >= 0)
        const horInView = (rect.left <= window.innerWidth) && ((rect.left + rect.width) >= 0)
        return (vertInView && horInView)
    }

    this.elms = document.querySelectorAll('.showOnScreen')

    window.addEventListener("scroll", () => {
        this.elms.forEach(elm => isInViewport(elm) ? elm.classList.add('visible') : elm.classList.remove('visible'))
    })
}

export default inViewport
Biomehanika
  • 1,530
  • 1
  • 17
  • 45
  • 1
    This code will result in performance issues, because you are triggering a reflow every time the scroll listener fires. The Intersection Observer used in the other answer has been invented exactly for this. – Fabian von Ellerts Dec 08 '20 at 10:58
  • 1
    Thank you @FabianvonEllerts, totally agree. Just checked it. – Biomehanika Dec 09 '20 at 11:42