Smooth Scrolling with React & Framer Motion
Tags: react, webdev, tutorial, javascript
Published: 2023-08-14 20:57:34


Scrolling through a website especially with a notched mouse wheel is typically jumpy and harder to navigate.

Smooth Scrolling, or spring scrolling adds an animated touch to the traditional mouse scroll.

If you've never experienced smooth scrolling before, try out the [Live Demo](https://framersmoothscroll.ironcladdev.repl.co/).

## Concept

Consider the viewport window.  When a bunch of HTML elements combine to a size taller than the window, it overflows and can be accessed when you scroll down.

![Window with Content](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zgnvjx3fxhiaqzv66l91.png)

To override the default scroll, we need to wrap the content in a fixed element we can control.

![Controlled content wrapper](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/15xcq9p1pygyaolldbz1.png)

Finally, we will create an invisible spacer (div) equal to the **scroll height** of the content.  This will trigger the browser's default scroll bar.

![Invisible height spacer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ebvuop2fer9mco0gh6n1.png)

## Setup

Create a [React Typescript](https://replit.com/@replit/React-TypeScript?v=1) Repl on [Replit](https://replit.com) to get started.

Install [Framer Motion](https://framer.com/motion) with `npm install framer-motion`.

## The <SmoothScroll/> Component

The `<SmoothScroll/>` Component will wrap all the HTML elements we want to incorporate in the Smooth Scrolling effect.

```tsx
export default function SmoothScroll({
  children
}: {
  children: React.ReactNode;
}) {
  return <></>;
}
```

### Scroll & Spring Values

Import the following hooks from `framer-motion`.

```tsx
import { useScroll, useSpring, useTransform } from 'framer-motion';
```

In the `SmoothScroll` component, destructure `scrollYProgress` from the `useScroll` hook.

```tsx
const { scrollYProgress } = useScroll();
```

Next, use the `useSpring` hook to apply the smooth effect to the `scrollYProgress` value.

```tsx
const smoothProgress = useSpring(scrollYProgress, { mass: 0.1 })
```

### Content & Spacer

Add the `motion` component to the existing import from `framer-motion`.

```diff
- import { useScroll, useSpring } from 'framer-motion';
+ import { motion, useScroll, useSpring } from 'framer-motion';
```

Skip down to the component's `return` statement.  Return an empty `<div>` element and a `<motion.div>` element which renders the `children` prop as a child.

```tsx
return <>
  <div style={{ height: contentHeight }} />

  <motion.div
    className="scrollBody"
  >
    {children}
  </motion.div>
</>
```

Create a `contentRef` react reference via the `useRef` hook and a `contentHeight` state with `useState`.

```
import { useState, useRef } from 'react';

...

const contentRef = useRef<HTMLDivElement>(null);
const [contentHeight, setContentHeight] = useState(0);
```

Assign the content wrapper the `contentRef`.

```tsx
<motion.div
  className="scrollBody"
  ref={contentRef}
>
  {children}
</motion.div>
```

Use the `style` prop to make the spacer have a height of `contentHeight`.

```tsx
<div style={{ height: contentHeight }} />
```

### Resize Handler

When the window gets resized, the height of the content content is likely to change.  Since the `contentHeight` value is a state, we will need to update it whenever the window resizes, and when the `contentRef` reference updates.

Start by importing the `useEffect` hook from `react`.

```diff
- import { useState, useRef } from 'react';
+ import { useEffect, useState, useRef } from 'react';
```

Within the useEffect hook, create and call a handler to set the `contentHeight` value to `contentRef.current.scrollHeight`.

Add `contentRef` to the dependency array.

```tsx
useEffect(() => {
  const handleResize = () => {
    if (contentRef.current) {
      setContentHeight(contentRef.current.scrollHeight)
    }
  }

  handleResize();
}, [contentRef]);
```

Finally, add a `resize` event listener to `window` in the useEffect hook, and return a disposer function.

```tsx
useEffect(() => {
  ...

  window.addEventListener("resize", handleResize);

  return () => {
    window.removeEventListener("resize", handleResize);
  }
}, [contentRef, children]);
```

## Put it all together

Import the `useTransform` hook from `framer-motion`.

```diff
- import { motion, useScroll, useSpring } from 'framer-motion';
+ import { useTransform, motion, useScroll, useSpring } from 'framer-motion';
```

Create a constant `y` and set it to the `useTransform` hook with `smoothProgress` as the initial value.

```tsx
const y = useTransform(smoothProgress, value => {
  return value;
});
```

Visualizing how to calculate the scroll, we will be subtracting the **viewport height** from the content container's **scroll height**, multiplying it by -1, and then by `value`.

![Scroll Visualization](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b3nmxl6jy4hfh2c1q355.png)

```tsx
const y = useTransform(smoothProgress, value => {
  return value * -(contentHeight - window.innerHeight);
});
```

Use the `y` transformed value in the content container.

```diff
<motion.div
  className="scrollBody"
  style={{ y }}
  ref={contentRef}
>
  {children}
</motion.div>
```

## Styles

I already fought CSS so you won't have to.  Simply copy and paste the bare minimum CSS over and the scroll component will be ready to roll.

```css
.scrollBody {
  width: 100vw;
  position: fixed;
  top: 0;

  display: flex;
  flex-direction: column;
}
```

## Complete 🎉

That's it!  All you have to do is add a bunch of HTML elements within the `<SmoothScroll />` component.

Nothing better than fifty `<h1>Lorem Ipsum</h1>`s in your codebase.

- Source Code: https://replit.com/@IroncladDev/FramerSmoothScroll?v=1
- Live Demo: https://framersmoothscroll.ironcladdev.repl.co

---

Thanks for reading.  If you have any feedback or recommendations for this article, I'd love to hear it in the comments.

Let's get in touch 🤝

 - 🐱 [Github](https://github.com/Conner1115)
 - 🦜 [Twitter](https://twitter.com/IroncladDev)
 - ⠕ [Replit](https://replit.com/@IroncladDev)
 - 🌐 [Website](https://www.connerow.dev)