How to animate scroll position in React
Tags: react, webdev, animation, tutorial
Published: 2020-06-16 11:16:44

---
title: How to animate scroll position in React
published: true
date: 2020-06-15 08:53:46 UTC
tags: react,webdev,animation,tutorial
canonical_url: https://blog.sethcorker.com/scroll-animation-with-framer-motion
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/tk5h8z3gz7n4rb5uu6zh.png
series: Framer Motion Animation
---

## What we’re animating in React

I’ll walk you through how I created the following animation with Framer Motion and React. It’s shows an envelope which as you scroll down, a letter slides out before sliding down over the envelope.

**See the [letter animation example in Framer Motion](https://framer-motion-stories.sethcorker.com/?path=/story/framer-motion-scroll--letter).**

If the iframe above isn’t working, you can also see [the letter animation example in Framer Motion here](https://framer-motion-stories.sethcorker.com/?path=/story/framer-motion-scroll--letter).We’re creating a simple effect where a letter looks like it’s being drawn from an envelope. The animation happens when scrolling up or down.

If you’re looking for some more guides with examples of how to use Framer Motion, take a look at how you can [create a spinner loading animation](https://blog.sethcorker.com/spinner-animation-in-react) or a [tap to expand animation](https://blog.sethcorker.com/react-animation-tap-to-expand)

## How to control animations on scroll with `useViewportScroll`

The `useViewportScroll` hook is one of my favourite features of Framer Motion. It allows us to control animations based on the scroll position.The way achieve this simple letter opening animation is by mapping time to the scroll position so when the scrollbar is at the top of the page, our animation is at the first frame.When the scrollbar is at the bottom of the page, our animation is at the last frame. Scrolling back up the page will reverse the animation.This is a simple effect but it’s possible to create complex animations and base them entirely on where the X and Y scroll position is.

### How do we use it?

For our letter example, we are basing the animation on just the Y position so we just use the hook like so:

```js
const { scrollYProgress } = useViewportScroll()
```

This hook returns `scrollY`, `scrollX`, `scrollYProgress` and `scrollXProgress`. I chose not to use `scrollY` because I don’t really care about how many pixels we’ve scrolled down the page.I just want to know where we are in the animation from 0 to 100% complete that’s why I use `scrollYProgress` which gives us a value between 0 and 1, then we transform that however we need to with the `useTransform` hook below.

## What `useTransform` does and how to use it

The `useTransform` hook is useful for transforming one value to another. What does this mean?It allows us to take our `scrollYProgress` which is between 0 and 1 and get a different set of values like so:

```js
const { scrollYProgress } = useViewportScroll()
const scaleAnim = useTransform(scrollYProgress, [0, 0.5, 1], [1, 1, 1.5])
const yPosAnim = useTransform(scrollYProgress, [0, 0.4, 1], [0, -250, -100])
```

What you’ll notice is that the first argument is `scrollYProgress`, this is what we want to transform.The second argument is a series of numbers that we want to transform between. We know `scrollYProgress` can be 0 at the minimum (we haven’t scrolled down the page at all) and 1 at the maximum (we reached the bottom of the page).So why do we use three values? Well, it’s because the animation we want to create has three distinct keyframes.So if look at his example for our Y position, `useTransform(scrollYProgress, [0, 0.4, 1], [0, -250, -100])`, we are saying at the beginning Y position 0 should transform to 0. No change.When we are 40% down the page (0.4), the Y position should be -250px upwards. Finally, when we reach the end of the page, the Y position should be at -100px.

### Tip for using the `useTransform` hook

When you use the `useTransform` hook this way, make sure you have the same number of input values as output values so if you need three keyframes then make sure you use two arrays of values as the second and third arguments.

## Breakdown of the letter animation example

### The structure of the example

Let’s walk through the example, I’ll breakdown the steps used to create letter animation and why certain decisions were made.I’m using CSS-in-JS to keep the style as close to the code as possible, you don’t have to do this and Framer Motion doesn’t demand you do it either.It’s possible to use CSS modules, plain old CSS or a combination of all the above.

#### Laying out the scene

First, I created some high level components to match the mental model of what we’re trying to achieve. An envelope with a letter inside it.

```jsx
<div style={letterSceneStyle}>
  <Envelope>
    <Letter />
  </Envelope>
</div>
```

The style for the container div is to give us enough space to scroll. A larger height will make the animation slower because the user has to scroll more for it to reach 100%. I chose a comfortable speed/viewport size of `200vh`.

```js
const letterSceneStyle = {
  height: "200vh",
}
```

#### Creating the envelope

Now we get into how the animation works. The only prop the envelope accepts is `children`, in our case. This is the `<Letter />`.A benefit of having the letter within the envelope is any animation we apply to the envelope will affect the letter.

```jsx
function Envelope({ children }) {
  const [ffLayer, setFfLayer] = useState(0)
  const { scrollYProgress } = useViewportScroll()
  const scaleAnim = useTransform(scrollYProgress, [0, 0.5, 1], [1, 1.2, 0.8])
  const yPosAnim = useTransform(scrollYProgress, [0, 0.5, 1], [0, 100, 200])
  const zRotAnim = useTransform(scrollYProgress, [0, 0.5, 1], [0, 3, 0])
  scrollYProgress.onChange(x => {
    setFfLayer(x > 0.4 ? -1 : 0)
  })
  return (
    <motion.div
      style={{
        ...envelopeStyle,
        scale: scaleAnim,
        y: yPosAnim,
        rotateZ: zRotAnim,
      }}
    >
      {children}
      <div style={{ ...frontfaceStyle, zIndex: ffLayer }}>
        <button onClick={() => window.scrollTo(0, 1500)}>Open Me</button>
      </div>
    </motion.div>
  )
}
```

We make extensive use of the `useTransform` hook to transform `scrollYProgress` into the values we need.In the case of `scaleAnim`, we want it to start at 100% scale then get larger halfway through the animation and small at the end.We also move the envelope down the screen with `yPosAnim` and perform a slight tilt with `zRotAnim`. To hook up the animation, we simply use a `<motion.div/>` component and set the `style` prop.If any of the values change, we’ll see a smooth animation thanks to Framer Motion. There are a few other things we need to do to achieve the effect which isn’t immediately obvious.

##### What’s the `ffLayer` state?

The effect I wanted to achieve is a letter being pulled out of an envelope so `ffLayer` is to keep track of the front face of the envelope. The reason we need this is so that after the letter is ‘pulled’ out, it can then slide down over the envelope.The `scrollYProgress` is a motion value so we can add an `onChange` handler to trigger the state change 50% though the animation.Once we are half-way though, we change the `z-index` of the envelope front face so it will be behind the letter.

##### How does the button scroll cause a smooth scroll?

As a way to showcase the animation, I added `<button onClick={() => window.scrollTo(0, 1500)}>Open Me</button>` to the envelope to allow for a smooth scroll.This works by scrolling the page down 1500px. Just this alone _won’t_ give us the effect we need. The next step is to make sure we add the following CSS.

```css
html {
  scroll-behavior: smooth;
}
```

#### Creating the letter

The letter animation makes use of the same hooks we’re familiar with from before. This time we animate the scale and the Y position.

```jsx
function Letter() {
  const { scrollYProgress } = useViewportScroll()
  const scaleAnim = useTransform(scrollYProgress, [0, 0.5, 1], [1, 1, 1.5])
  const yPosAnim = useTransform(scrollYProgress, [0, 0.4, 1], [0, -250, -100])
  return (
    <motion.div
      style={{
        ...letterStyle,
        scale: scaleAnim,
        y: yPosAnim,
      }}
    >
      {/* The contents of the letter goes here */}
    </motion.div>
  )
}
```

### Setup styles with CSS-in-JS

The most important thing to achieve the envelope and letter effect are some basic styles before we apply the animation.

#### Envelope Styles

We set a defined with and height of the envelope and position it on the page. Notice, we always want to see the animation take place in the center of the screen regardless of where the user has scrolled to so we use `position: fixed`.The front face style is very simple too. It just needs to be the same size as the envelope and have a `backgroundColor` so it hides the letter while still inside the envelope.

```js
const envelopeStyle = {
  width: "28rem",
  height: "15rem",
  scale: 1,
  position: "fixed",
  top: "10rem",
  left: "calc(50% - 14rem)",
  boxShadow: `rgba(0, 0, 0, 0.5) 0px 0px 150px 10px`,
}

const frontfaceStyle = {
  width: "100%",
  height: "100%",
  backgroundColor: "#debda1",
  position: "absolute",
  left: 0,
  top: 0,
  display: "flex",
  justifyContent: "center",
  alignItems: "center",
}
```

#### Letter style

The only important properties of the letter is the dimensions and position.The position is absolute so we can position it within the envelope. The size is also slightly smaller so it looks as though the letter fits within the envelope.

```js
const letterStyle = {
  width: "calc(100% - 1rem)",
  height: "calc(100% - 1rem)",
  top: "0.5rem",
  left: "0.5rem",
  backgroundColor: "#f8efd5",
  overflow: "hidden",
  padding: "1rem",
  boxSizing: "border-box",
  position: "absolute",
}
```

## Where to go from here

Although it’s a silly example, a letter coming out of an envelope, this is a good example to see how to take advantage of the capabilities Framer Motion offers.If you want a more practical application of `useViewportScroll`, you could do one of the following:

- Show a progress bar of how far through someone is reading a webpage.
- Highlight headings in a sidebar depending on if they are reading that section or not.
- Have elements appear and disappear easily on scroll

The possibilities are boundless, think of some cool ways you can orchestrate animations based on the scroll position.

## Resources

- [`useViewportScroll` on Framer Motion API docs](https://www.framer.com/api/motion/motionvalue/#useviewportscroll)
- [`useTransform` on Framer Motion API docs](https://www.framer.com/api/motion/motionvalue/#usetransform)
- [The letter animation example in Framer Motion](https://framer-motion-stories.sethcorker.com/?path=/story/framer-motion-scroll--letter)
- [The full source code for the letter example on GitHub](https://github.com/Darth-Knoppix/framer-motion-stories/blob/master/src/scenes/letter/index.js)