# Advanced Scroll Effects in Framer

> Three years ago I wrote an article for [Framer](https://www.framer.com?via=giles_perry) on how to create scroll interaction effects. Much has happened since then, in particular the ability to publish fully functioning websites.

> So how do the techniques I explored fit this new world?

> We now have two contexts where you might want to apply scrolling effects: scrolling of the whole page relative to the viewport, and scrolling of content that overflows its container. I will concentrate on page scrolling, but the techniques for element scrolling are the same, so look out for a pointer on this.

> My focus will be on code, but many of the effects I first wrote about are now more easily applied through native features. I will speak about these options too.

### In this article you will learn how to...

* [Create parallax](#heading-parallax)
    
* [Use scrolling to drive the transformation of another layer](#heading-more-scroll-driven-effects)
    
* [Handle element scrolling](#heading-now-is-a-good-time-to-talk-about-element-scrolling)
    
* [Build a dynamic nav bar](#heading-dynamic-nav-bar)
    
* [Trigger animations at set scroll positions](#heading-triggering-animations-at-set-scroll-positions)
    
* [Apply effects while an element is in view](#heading-viewport-awareness)
    
* [Build a scroll-away nav bar](#heading-scroll-away-nav-bar)
    

---

# First let’s talk theory

Behind the scenes, the things you create in Framer are powered by HTML, CSS and JavaScript. Framer is based on React, a JavaScript library for building user interfaces. Everything you make in Framer is a [React component](https://reactjs.org/docs/components-and-props.html).

Components have properties such as height, width, or scale. We call them `props`. When you add a frame to your project and pick a background colour in the Properties panel, you are setting the frame’s `props`.

Framer lets you apply code to a layer that will override the layer’s props when it is displayed in the *Preview* window, and on your published site. [Code Overrides](https://www.framer.com/developers/guides/overrides/) are incredibly powerful, so it's worth getting to know them. Through overrides you have access to pretty much anything the web is capable of.

## We'll start with something simple

Let's say you want to override a layer's height.

Height is a CSS style attribute, so to make the layer 100 pixels high we need to update its `style` prop using the value `{ height: 100 }`.

Here’s the **Code Override** to do exactly that:

```typescript
import type { ComponentType } from "react"

export function withSpecificHeight(Component): ComponentType {
    return (props: any) => {
        return <Component {...props} style={{ ...props.style, height: 100 }} />
    }
}
```

If you are new to coding, being able to read and understand even a simple example like this means taking on JavaScript, TypeScript and React all at once.

## Let's pick it apart

A React component is simply a function that returns a block of HTML.

Overrides are a little bit more complicated. In React, an override is what is known as a [higher-order component](https://reactjs.org/docs/higher-order-components.html). It’s **a function that takes another React component** as an input **and returns a new component**. The override uses this technique to modify the original component’s props.

So, when you add an override to a layer, you are replacing the underlying React component with one that has been modified. By convention, override functions have “with” as a prefix because they return a component *with* a modification.

Let's replay our example one step at a time:

```plaintext
import type { ComponentType } from "react"
```

Import a type definition, `ComponentType`, provided by React. We'll use this to let the code know that the override function returns a component.

```plaintext
export function withSpecificHeight(Component): ComponentType
```

Export a function component called `withSpecificHeight`. We need to export it so Framer can access it. The function has a single argument: `Component`. This is the component that Framer will pass into the function.

How about the `: ComponentType` tacked on the end? That’s TypeScript. It’s a type annotation specifying that this function returns a component.

Everything in the first set of curly braces is the code block that runs when you call the function. It's pretty simple. It just returns the following React function component:

```typescript
(props: any) => {
    return <Component {...props} style={{ ...props.style, height: 100 }} />
}
```

This function defines what will actually be rendered:

`<Component />` is our original component. Everything else describes the `props` we want to apply this time round.

`{...props}` is a shorthand syntax for listing out and applying all of the original `props` intact. Because `style` is set after this, its value gets overridden:

```plaintext
style={{ ...props.style, height: 100 }}
```

Similarly, we want to override `height` without affecting any other styles: `...props.style` lists out and applies any existing styles, then we override the `height`.

**Boom! Now you can code. Let's get going!**

> All my examples use Overrides. If you are new to Framer you can read more about Overrides in the [Framer Developers Guides](https://www.framer.com/developers/guides/overrides/). If you need an introduction to React, you might enjoy reading the [Framer Guide to React](https://www.framer.com/books/framer-guide-to-react/):

---

# Parallax

%[https://codesandbox.io/embed/parallax-o1ofcv?fontsize=14&hidenavigation=1&hidedevtools=1&theme=dark&view=preview] 

Parallax is an effect applied to content as it scrolls that creates the illusion of depth by moving background layers more slowly than layers in the foreground. **Parallax is all about relative movement**.

The easy route to parallax is using Framer’s native scroll effects. Speed Effects allow you to define the scrolling speed of any given layer. Read Framer’s [documentation](https://www.framer.com/learn/scroll-effects/) to find out how.

This is great. If vertical parallax is all you need you are good to go. For anything more bespoke you'll need an override.

In a lot of cases, the code is surprisingly simple. We want to establish a relationship between the distance scrolled and another value. We have already seen how an override can modify component properties, so we need two more things.

1. A way to track the scroll amount.
    
2. A way to the transform this valve into the value we want to apply to the layer.
    

Framer’s animation library, [Motion](https://www.framer.com/motion/), provides utility functions for each of these.

## Keeping track of the scroll distance

The first is [useScroll](https://www.framer.com/docs/use-scroll/).

This function returns a value that stays synced to the distance the page is scrolled. Before we can call it in our override function, we need to import it from the library:

```plaintext
import { useScroll } from "framer-motion"
```

Now we can get `scrollY`, the vertical scroll distance in pixels:

```plaintext
const { scrollY } = useScroll()
```

`scrollY` is special kind of variable called a [MotionValue](https://www.framer.com/docs/motionvalue/) that Framer uses to track the state and velocity of a value that animates or changes rapidly. As you scroll, `scrollY` will magically update any property we connect to it.

## Transforming MotionValues

We could apply the value in `scrollY` directly, but in most cases we need to transform it into another value. For that we need [useTransform](https://www.framer.com/docs/use-transform/).

`useTransform` creates a new MotionValue by transforming the output of a MotionValue we pass to it.

Say we want a layer to drift horizontally as we scroll down the page, translating it 1px left along the x-axis for every 2px we scroll down. i.e. half the scroll speed in a negative x direction.

We can transform `scrollY` into the `x` value we need like this:

```plaintext
const x = useTransform(scrollY, (value) => -value / 2)
```

All that's left to do is to use `x` to translate the layer along the x-axis using the CSS `transform` property.

> **Motion** gives us easy access to these properties via [enhanced style props](https://www.framer.com/docs/component/##transform) and `x`, `y`, and `z` Translate shortcuts.

Here’s the override in its entirety:

```typescript
import type { ComponentType } from "react"
import { useScroll, useTransform } from "framer-motion"

export function withParallax(Component): ComponentType {
    const speed = 1 / 2
    return (props: any) => {
        const { scrollY } = useScroll()
        const x = useTransform(scrollY, (value) => -value * speed) // scrolling down translates left
        return <Component {...props} style={{ ...props.style, x: x }} />
    }
}
```

---

# More scroll driven effects

The same technique can be used to make scrolling drive all kinds of properties. Here’s another override to add to your collection. Use it to drive a Lottie animation or scrub through a video (both components have a `progress` prop and can be added from the Insert panel).

```typescript
// Scrub through a video or drive a Lottie animation by scrolling
export function withScrolledProgress(Component): ComponentType {
    const startY = 0 // scroll position when animation starts
    const distance = 1000 // scroll distance after which animation ends
    const endY = startY + distance

    return (props) => {
        const { scrollY } = useScroll()
        const progress = useTransform(scrollY, [startY, endY], [0, 1])

        return <Component {...props} progress={progress} />
    }
}
```

You will notice we are passing arguments into `useTransform` in a different format. Instead of a function for transforming the input value, we use arrays to specify input and output ranges. These determine the output required at specific points. Values in between are interpolated.

---

# Now is a good time to talk about element scrolling

The main difference, in the case of element scrolling, is we need a `ref` to identify the scrolling element.

[Refs provide a way for React to access nodes in the DOM](https://reactjs.org/docs/refs-and-the-dom.html).

We also need two overrides:

* One to identify the scroll container.
    
* One to apply the transformed property to the element we want affect.
    

Let's give it a try... First import a function from React for creating refs:

```plaintext
import { createRef } from "react"
```

The `ref` has to be created outside of the overrides so that both overrides can use the same `ref`:

```plaintext
const ref = createRef<HTMLDivElement>()
```

Here’s the override to attach the `ref` to a component. Apply this to your scroll container:

```typescript
export function withScrollRef(Component): ComponentType {
    return (props) => {
        return <Component {...props} ref={ref} />
    }
}
```

To track the scroll position of a scrollable element, instead of the page, we pass the `ref` to `useScroll`'s `container` option:

```plaintext
const { scrollY } = useScroll({
    container: ref
})
```

In all other respects, our parallax override is identical to the one above:

```typescript
export function withElementParallax(Component): ComponentType {
    const speed = 1 / 2
    return (props: any) => {
        const { scrollY } = useScroll({
            container: ref
        })
        const x = useTransform(scrollY, (value) => -value * speed) // scrolling down translates left
        return <Component {...props} style={{ ...props.style, x: x }} />
    }
}
```

Here’s everything we need in one file:

```typescript
import type { ComponentType } from "react"
import { createRef } from "react"
import { useTransform, useScroll } from "framer-motion"

// create a ref so we can attach it to the scroll container
const ref = createRef<HTMLDivElement>()

// apply this to the element being scrolled
export function withScrollRef(Component): ComponentType {
    return (props) => {
        return <Component {...props} ref={ref} />
    }
}

// apply this to the element with the scroll effect
export function withElementParallax(Component): ComponentType {
    const speed = 1 / 2
    return (props: any) => {
        const { scrollY } = useScroll({
            container: ref
        })
        const x = useTransform(scrollY, (value) => -value * speed) // scrolling down translates left
        return <Component {...props} style={{ ...props.style, x: x }} />
    }
}
```

The remaining examples will stick to viewport scrolling, but you can adapt them to element scrolling using this framework.

---

# Dynamic nav bar

%[https://codesandbox.io/embed/dynamic-nav-bar-mhsoy7?fontsize=14&hidenavigation=1&hidedevtools=1&theme=dark&view=preview] 

A nav bar that collapses when you scroll is another great use case for scroll driven transformations. The override we need is very similar to `withScrolledProgress`.

By default, output from `useTransform` is clamped to stay within the given range. Using it this way, as we have so far, would allow us to scroll between a maximum and minimum height.

However, apps often have a bar that shrinks to a minimum size when the user scrolls up, but stretches as far as you are able to pull it in the other direction. If that's the case, we only want to clamp the value in one direction. The trick to making that work is unclamping the transform while repeating the final input and output values.

Here’s what that looks like:

```typescript
export function withDynamicNavBar(Component): ComponentType {
    // Value being driven by scrolling (e.g. height)
    const initialValue = 140
    const finalValue = 88

    const speed = 1
    const scrollDistance = (initialValue - finalValue) / speed

    const startY = 0 // scroll position when transition starts
    const endY = startY + scrollDistance

    return (props: any) => {
        const { scrollY } = useScroll()
        const scrollOutput = useTransform(
            scrollY,
            [startY, endY, endY],
            [initialValue, finalValue, finalValue],
            {
                clamp: false,
            }
        )
        return <Component {...props} style={{ ...props.style, height: scrollOutput }} />
    }
}
```

---

# Triggering animations at set scroll positions

Another feature we see in dynamic navigation bars is a title, which fades in when the bar reaches its minimum size. **How can we animate the opacity of the title** so it appears only when `scrollY` passes a threshold?

## It's time for a bit more theory…

When you imaging coding an interaction, it feels like you need code telling the application exactly what to do:

> Change the opacity of my title to a new value when I scroll past a threshold.

Code like this is *imperative*.

React requires a new way of thinking. Instead of specifying what to do, the code is *declarative*; it tells the application how to be:

> Be transparent below the threshold, and be opaque above the threshold.

React takes care of everything else, making sure the opacity changes when the condition changes.

To make this happen our code needs three parts:

* A variable to keep track of the state of our application: is `scrollY` past a limit, `true` or `false`?
    
* Something to listen for changes to `scrollY` and update the application state if the limit is passed.
    
* A function telling the title to animate to either 1 or 0 opacity depending the current state.
    

We are going to need a couple of [React Hooks](https://reactjs.org/docs/hooks-intro.html) to get this done. Let’s import them:

```plaintext
import { useState, useEffect } from "react"
```

## Adding a state variable

We need a Boolean (`true` or `false`) state variable where we will keep a record of whether the scroll distance **is past** the **threshold** or not. The `useState` Hook gives us a way to create and update a state variable:

```plaintext
const [isPastThreshold, setIsPastThreshold] = useState(false)
```

`isPastThreshold` is the state variable and `setIsPastThreshold` is the setter function that allows us to update it. The initial state is `false`.

## Listening for changes

Listeners can be added to `MotionValue`s with the `onChange` method. This method passes the latest value into a function we provide every time the `MotionValue` changes.

```plaintext
scrollY.onChange((latest) => setIsPastThreshold(latest > thresholdY))
```

We are required by React to add the listener **after the component is rendered**. Long story short to run some code after rendering we need to add an *Effect*. That’s where `useEffect` comes in:

```typescript
useEffect(
    () => scrollY.onChange((latest) => setIsPastThreshold(latest > thresholdY)),
    []
)
```

## Telling the component how to be

This is as simple as setting opacity on the component's `animate` prop using a conditional value.

```plaintext
animate={{ opacity: isPastThreshold ? 1 : 0 }}
```

When `isPastThreshold` is `true` the opacity value will animate to 1, otherwise it will animate to 0.

```typescript
export function withScrollToggledState(Component): ComponentType {
    const thresholdY = 100 // set the scroll position where you want the state change
    return (props) => {
        const { scrollY } = useScroll()
        const [isPastThreshold, setIsPastThreshold] = useState(false)
        useEffect(
            () =>
                scrollY.onChange((latest) =>
                    setIsPastThreshold(latest > thresholdY)
                ),
            []
        )
        return (
            <Component
                {...props}
                animate={{ opacity: isPastThreshold ? 1 : 0 }}
            />
        )
    }
}
```

Get used to seeing this:

`const property = condition ? valueWhenTrue : valueWhenFalse`

In React, this is your best friend…

## Triggering a variant switch

Now we know how to trigger state changes, we can use the same trick on the `variant` prop:

```plaintext
variant={isPastThreshold ? "Second" : "First"}
```

When you try this for the first time, you see the power of declarative code in sharp focus. Set one simple condition and Magic Motion will handle everything.

> This is another scenario that can be handled natively. You’ll find the Scroll Variant option when you add an Effect to any component with more than one variant. Read Framer’s documentation to learn about native [Scroll Effects](https://www.framer.com/learn/scroll-effects/).

---

# Viewport awareness

%[https://codesandbox.io/embed/scale-b30sjs?fontsize=14&hidenavigation=1&hidedevtools=1&theme=dark&view=preview] 

So far we have controlled when effects are applied by hard-coding the scroll position. This approach can be limiting. If we want the same effect on a number of layers, we need a bespoke override for each. If we change the element's position, we need to update our code. Even worse, if our layout is responsive, we may not know element positions upfront.

Luckily, Motion has our back.

## Animate while in view

Triggering animations as an element enters the viewport is a simple as setting Motion's `whileInView` prop.

Add the `viewport` prop if you need more control over how the viewport is detected.

Here's an override that animates an element's `opacity` the first time the element appears in the viewport:

```typescript
export function withAnimateWhileInView(Component): ComponentType {
    return (props) => {
        return (
            <Component
                {...props}
                initial={{ opacity: 0 }}
                whileInView={{ opacity: 1 }}
                viewport={{ once: true }}
            />
        )
    }
}
```

> You can also use `whileInView` for animating between variants. [Take a look at the docs](https://www.framer.com/docs/scroll-animations/#scroll-triggered-animations) to learn more about `whileInView` and its corresponding `viewport` options.

## How about scroll-driven animations?

You can [track an element's progress within the viewport](https://www.framer.com/docs/use-scroll/##element-position) by passing its `ref` to `useScroll`'s `target` option.

Let's see if we can reproduce the scaling effect shown in the demo above.

If we define the `start` and `end` of the viewport from top to bottom, and apply the same logic to the circles, then each circle should:

* start animating when its `end` intersects with the viewport's `end`,
    
* and finish when its `start` intersects with the viewport's `start`.
    

In other words we want make each circle a scroll `target`, and measure its **progress** as it scrolls from `"end end"` to `"start start"`.

Here is an override you can attach to each circle to make that happen:

```typescript
import type { ComponentType } from "react"
import { useRef } from "react"
import { useScroll, useTransform } from "framer-motion"

export function withScrollLinkedScale(Component): ComponentType {
    return (props: any) => {
        const ref = useRef(null)

        const { scrollYProgress } = useScroll({
            target: ref,
            offset: ["end end", "start start"],
        })

        const scale = useTransform(scrollYProgress, [0, 0.5, 1], [0.5, 1, 0.5])

        return (
            <Component {...props} ref={ref} style={{ ...props.style, scale }} />
        )
    }
}
```

**There are a few things to call out:**

We met `ref`s earlier. This time we don't need to share the `ref` so we create it inside the override with React's `useRef` hook.

```plaintext
const ref = useRef(null)
```

Instead of the absolute scroll position, in pixels, we tell `useScroll` to return `scrollYProgress`: a `MotionValue` between `0` and `1`. Also, we pass in `target` and `offset` options.

```plaintext
const { scrollYProgress } = useScroll({
    target: ref,
    offset: ["end end", "start start"],
})
```

As `scrollYprogress` progresses from `0` when the circle enters the viewport, to `0.5` in the centre of the viewport, to `1` at the end, we transform its scale from `0.5` to `1` and back to `0.5`.

```plaintext
const scale = useTransform(scrollYProgress, [0, 0.5, 1], [0.5, 1, 0.5])
```

---

# Scroll-away nav bar

%[https://codesandbox.io/embed/scroll-away-9i8k5k?fontsize=14&hidenavigation=1&hidedevtools=1&theme=dark&view=preview] 

I'm going to step up the pace a little and look at a more complex interaction: a commonly seen pattern where a nav bar slides off screen when you scroll down the page and slides back into view when you scroll back.

Thinking declaratively… we need a condition to tell us this: **is** the nav bar **in view** or not?

This will depend on two things:

* **is** the user **scrolling back** or not?
    
* **is** the page **at** the **top**? (because we always want to show the nav bar when we are at the top of the page)
    

Which gives us three state variables: `isInView`, `isScrollingBack` and `isAtTop`.

Scroll direction can be calculated by checking if the velocity of `scrollY` is positive or negative.

```plaintext
const { scrollY } = useScroll()
const scrollVelocity = useVelocity(scrollY)
```

We need to listen for changes in scroll velocity in one Effect.

```typescript
useEffect(
    () =>
        scrollVelocity.onChange((latest) => {
            if (latest > 0) {
                setIsScrollingBack(false)
                return
            }
            if (latest < -threshold) {
                setIsScrollingBack(true)
                return
            }
        }),
    []
)
```

In another we set `isAtTop`

```plaintext
useEffect(
    () => scrollY.onChange((latest) => setIsAtTop(latest <= 0)),
    []
)
```

And if either `isScrollingBack` or `isAtTop` changes we have a final Effect to update: `isInView`.

```plaintext
useEffect(
    () => setIsInView(isScrollingBack || isAtTop),
    [isScrollingBack, isAtTop]
)
```

Here's what that all comes to in full:

```typescript
export function withScrollAway(Component): ComponentType {
    const slideDistance = 100 // if we are sliding out a nav bar at the top of the screen, this will be it's height
    const threshold = 500 // only slide it back when scrolling back at velocity above this positive (or zero) value

    return (props) => {
        const { scrollY } = useScroll()
        const scrollVelocity = useVelocity(scrollY)

        const [isScrollingBack, setIsScrollingBack] = useState(false)
        const [isAtTop, setIsAtTop] = useState(true) // true if the page is not scrolled or fully scrolled back
        const [isInView, setIsInView] = useState(true)

        useEffect(
            () =>
                scrollVelocity.onChange((latest) => {
                    if (latest > 0) {
                        setIsScrollingBack(false)
                        return
                    }
                    if (latest < -threshold) {
                        setIsScrollingBack(true)
                        return
                    }
                }),
            []
        )

        useEffect(
            () => scrollY.onChange((latest) => setIsAtTop(latest <= 0)),
            []
        )

        useEffect(
            () => setIsInView(isScrollingBack || isAtTop),
            [isScrollingBack, isAtTop]
        )

        return (
            <Component
                {...props}
                animate={{ y: isInView ? 0 : -slideDistance }}
                transition={{ duration: 0.2, delay: 0.25, ease: "easeInOut" }}
            />
        )
    }
}
```

---

## Time to put it all together

**Congratulations 🎉**. You are a scroll master. With the techniques and concepts you’ve learned here, you have the building blocks of almost every kind of scroll interaction.

To see these overrides in action and more [check out this example project](https://tools-paint-059317.framer.app).

[![Partner Banner Color@2x.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1669125852141/79MLwBwLG.png align="left")](https://www.framer.com?via=giles_perry)

---

## A bit about me

My name is Giles Perry. I’m a Principal Product Designer at Skyscanner. Check out my [website](https://gilesperry.info) or find me on [LinkedIn](https://www.linkedin.com/in/gilesperryuserexperience/).

Like this article and **want to say thanks**? Why not [buy me a coffee](https://www.paypal.me/perrysmotors/2) ☕️?
