DEV Community

Marc Mintel
Marc Mintel

Posted on • Originally published at mintel.me on

How to create Scroll-Linked Animations with React and Framer Motion πŸ’ƒπŸ»πŸ•ΊπŸ»

How to create Scroll-Linked Animations with React and Framer Motion πŸ’ƒπŸ»πŸ•ΊπŸ»

Scroll-linked animations are a popular technique that links animation to the user's scrolling on the page. As you scroll down the page, different elements will animate in or out, giving your website a unique and dynamic feel.

In this tutorial, we'll be using React and Framer Motion to create a scroll-linked animation. Framer Motion is a powerful animation library that makes it easy to create fluid and beautiful animations with React. We'll also be using TypeScript, a popular language that adds strong typing to JavaScript and can help catch errors before runtime.

The Code

Here's the TypeScript code we'll be working with:

import { motion, useScroll, useTransform } from 'framer-motion'
import { useElementViewportPosition } from '../hooks/useElementViewportPosition'
import React, { useRef, PropsWithChildren } from 'react'

export const ScrollReveal: React.FC<PropsWithChildren<{
  offset?: number
  distance?: number
}>> = ({ offset = 0, distance = 50, children }) => {
  const ref = useRef(null)
  const { position } = useElementViewportPosition(ref, offset)
  const { scrollYProgress } = useScroll()
  const opacity = useTransform(scrollYProgress, position, [0, 1])
  const y = useTransform(scrollYProgress, position, [distance, 0])

  return (
    <motion.div
      ref={ref}
      style={{
        opacity,
        y,
      }}
    >
      {children}
    </motion.div>
  )
}
Enter fullscreen mode Exit fullscreen mode

And here's the code for the useElementViewportPosition hook that we'll be using:

import { useState, useEffect, RefObject } from 'react'

export function useElementViewportPosition(
  ref: RefObject<HTMLElement>,
  offset = 0,
) {
  const [position, setPosition] = useState([0, 0])

  useEffect(() => {
    const update = () => {
      if (!ref || !ref.current) return
      const pageHeight = document.body.scrollHeight
      const start = ref.current.offsetTop
      const end = start + ref.current.offsetHeight

      setPosition([(start + offset) / pageHeight, (end + offset) / pageHeight])
    }

    update()

    document.addEventListener('resize', update)

    return () => {
      document.removeEventListener('resize', update)
    }
  }, [offset, ref])

  return { position }
}

Enter fullscreen mode Exit fullscreen mode

How It Works

So, how do these two pieces of code work together to create those scroll-linked animations? Let me break it down for you.

First, we're importing motion, useScroll, and useTransform from Framer Motion, and useElementViewportPosition from our own custom hook. We're also importing some stuff from React.

import { motion, useScroll, useTransform } from 'framer-motion'
import { useElementViewportPosition } from '../hooks/useElementViewportPosition'
import React, { useRef, PropsWithChildren } from 'react'

Enter fullscreen mode Exit fullscreen mode

Next, we're defining a new component called ScrollReveal. This component takes two optional props: offset and distance, which we'll explain in a bit. It also takes some children that we'll animate as the user scrolls.

export const ScrollReveal: React.FC<PropsWithChildren<{
  offset?: number
  distance?: number
}>> = ({ offset = 0, distance = 50, children }) => {
Enter fullscreen mode Exit fullscreen mode

Inside this component, we're creating a new ref with useRef(null). We're also calling our useElementViewportPosition hook and passing in our ref and offset. This hook will return a value called position.

Breaking Down the Code

Now that we've seen the code in action, let's break it down line by line.

import { motion, useScroll, useTransform } from 'framer-motion'
import { useElementViewportPosition } from '../hooks/useElementViewportPosition'
import React, { useRef, PropsWithChildren } from 'react'
Enter fullscreen mode Exit fullscreen mode

Here we have our imports. We're using Framer Motion, a popular React animation library, and importing motion, useScroll, and useTransform. We're also importing a custom hook called useElementViewportPosition from our ../hooks folder. Finally, we're importing React and a couple of types.

export const ScrollReveal: React.FC<PropsWithChildren<{
  offset?: number
  distance?: number
}>> = ({ offset = 0, distance = 50, children }) => {

Enter fullscreen mode Exit fullscreen mode

This line exports a React functional component called ScrollReveal. It's defined as a function that takes a prop object with optional offset and distance properties, as well as the children prop. The component will use these props to determine how much to reveal and how far to animate.

const ref = useRef(null)

Enter fullscreen mode Exit fullscreen mode

This line creates a React ref called ref that we'll use later to track the position of the component in the viewport.

const { position } = useElementViewportPosition(ref, offset)

Enter fullscreen mode Exit fullscreen mode

This line calls the useElementViewportPosition hook we imported earlier, passing in our ref and offset props. The hook returns an object with a position property that we'll use to animate the component.

const { scrollYProgress } = useScroll()

Enter fullscreen mode Exit fullscreen mode

This line calls another Framer Motion hook called useScroll, which returns an object with a scrollYProgress property that we'll use to animate the component.

useTransform does all the Magic!

The useTransform hook is part of the Framer Motion library, which is used in the ScrollReveal component.

The useTransform hook is used to create a relationship between the scroll position and an animated property of an element. In this case, it's used to adjust the opacity and y-position of the animated element (motion.div) as the user scrolls.

In more detail, useTransform takes three arguments: input, inputRange, and outputRange. The input argument is a value that changes over time, in this case the scrollYProgress from useScroll. The inputRange argument is an array that defines the range of the input value (e.g. [0, 1]), and the outputRange argument is an array that defines the range of values for the output (e.g. [distance, 0]).

const opacity = useTransform(scrollYProgress, position, [0, 1])
const y = useTransform(scrollYProgress, position, [distance, 0])
Enter fullscreen mode Exit fullscreen mode

These lines use the useTransform hook to create two new variables, opacity and y, that we'll use to animate the component. The useTransform hook takes three arguments: a value to transform (in this case scrollYProgress), the range of the input values (position), and the range of the output values ([0, 1] for opacity and [distance, 0] for y).

return (
    <motion.div
      ref={ref}
      style={{
        opacity,
        y,
      }}
    >
      {children}
    </motion.div>
  )

Enter fullscreen mode Exit fullscreen mode

Finally, we return a motion.div with our ref and two style properties, opacity and y, that we created using the useTransform hook. We also render the children prop inside the motion.div.

Conclusion

In this article, we've explored how to use TypeScript, React, and Framer Motion to create a scroll-linked animation. We've broken down the code line by line and explained what each part does. If you want to learn more about TypeScript, check out the official documentation. If you want to learn more about Framer Motion, check out the official documentation. Happy animating!

Top comments (0)