Mastering Scrolling in Framer X

15m

Intermediate

In this article we’ll look at the different ways we can use Framer’s scroll component to create the following:

  • Parallax scroll
  • Scroll gestures
  • Dynamic app bars
  • Sticky headers

Along the way, you’ll learn more about how React components work in Framer X. The code examples in this article rely on the new Framer Library API, which is now available with the latest version of Framer X.

First let’s talk about some theory

Behind the scenes, everything you see in Framer X is a React component. Components have properties such as height, width, and border radius. We call them props. When you add a frame to the canvas and pick a background color 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. An override is simply a function that returns new values for any properties you want to override. For example, to move a layer 100px down along the y-axis we need to set its y prop to 100.

Here’s the code override to do that:

import { Override } from "framer"
export function TranslateY(): Override {
return {
y: 100,
}
}
COPY

All my examples use overrides. If you are new to Framer X you can read more about code overrides in the Framer Docs. If you need an introduction to React, you might enjoy reading Koen Bok’s introduction to React for web designers.

Parallax

Let’s get our content scrolling

Drop a scroll component onto the canvas and connect it to a frame containing your scroll content. As you scroll, Framer moves the content frame relative to the scroll component. We can access the distance our content is offset through two props: contentOffsetX and contentOffsetY.

Parallaxes are all about relative movement. Let’s take a simple example where we are scrolling vertically and our parallax layer sits on the background of the scroll content. Under normal circumstances, the background layer moves with the main content. If we want it to move at half the speed of the main content then, for every 100px it is scrolled up, we need to add a downwards translation of 50px. In other words, whatever the current value of contentOffsetY, we need to take that value, halve it, and apply the transformed value to the parallax layer’s y property.

Keeping track of the scroll distance

Framer Library, allows us to track the state and velocity of a given property with a special variable called a MotionValue. We track contentOffsetY like this:

First create a MotionValue (set to zero initially):

const contentOffsetY = motionValue(0)
COPY

Then use it to override contentOffsetY in our scroll component:

export function TrackScroll(): Override {
return { contentOffsetY: contentOffsetY }
}
COPY

Whenever we scroll, the MotionValue will update with the content offset value.

Transforming MotionValues

The Framer Library also gives us a way to transform MotionValues with the useTransform function. useTransform creates aMotionValue by transforming the output of another MotionValue.

For example:

const y = useTransform(contentOffsetY, [0, -100], [0, 50])
COPY

When contentOffsetY is within the range 0 to -100, its value will be transformed to create a y value in the range 0 to 50.

The input range is negative because contentOffsetY is measured top down, so scrolling up gives us a negative offset.

By default y will be clamped so it is only transformed when contentOffsetY is within the input range. We want to transform y no matter how much we scroll. To achieve this, we need to pass the function an additional argument: {clamp: false}.

Here’s the complete code:

import { Override, motionValue, useTransform } from "framer"
const contentOffsetY = motionValue(0)
// Apply this override to your scroll component
export function TrackScroll(): Override {
return { contentOffsetY: contentOffsetY }
}
// Apply this override to your parallax layer
export function ParallaxLayer(): Override {
const y = useTransform(contentOffsetY, [0, -100], [0, 50], {
clamp: false,
})
return {
y: y,
}
}
COPY

More scroll-driven transformations

The same technique can be used to make vertical scrolling drive horizontal movement. Just apply a transformed MotionValue to x instead of y. When applied to a layer inside the scroll content, the layer could drift sideways as it scrolls up. But the layer doesn’t need to sit inside the scroll content to move with the parallax. Applying the below code to the y property of a layer on top of the scroll component would make it move with the scroll content, but at twice the speed.

useTransform(contentOffsetY, [0, 100], [0, 200], { clamp: false })
COPY

Dynamic app bar

This is another great use case for scroll-driven transformations. Here’s what you need to make an iOS app bars shrink from 140px to 88px high.

export function AppBar(): Override {
const height = useTransform(contentOffsetY, [0, -52], [140, 88])
return {
height: height,
}
}
COPY

Normally, iOS app bars shrink to a minimum size but, pulled in the other direction, they will stretch as far as you are able to pull them. In other words, we only want to clamp the value in one direction. Here’s a little trick to make that work:

const height = useTransform(contentOffsetY, [0, -52, -52], [140, 88, 88], { clamp: false })
COPY

Putting it all together:

import { Override, motionValue, useTransform } from "framer"
const contentOffsetY = motionValue(0)
// Apply this override to your scroll component
export function TrackScroll(): Override {
return { contentOffsetY: contentOffsetY }
}
// Apply this override to your app bar
export function AppBar(): Override {
const height = useTransform(contentOffsetY, [0, -52, -52], [140, 88, 88], {
clamp: false,
})
return {
height: height,
}
}
COPY

Triggering animations at set scroll positions

Another feature of app bars in iOS is the title, which fades in when the bar reaches it’s minimum size. How can we animate the opacity of the title so it appears only when contentOffsetY passes a threshold?

This is a good time for a bit more theory

So if you’ve come from Framer Classic you are about to discover a whole new programming model.

In Classic, code is imperative, it tells the prototype what to do:

When I scroll, if contentOffsetY is greater than a threshold, then animate the opacity of my title to a new value.

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

I want to animate my title depending on a condition.

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

So our code needs three parts:

  • A data object to keep track of the state of our application: true or false, is contentOffsetY past a limit?
  • Something to listen for changes to contentOffsetY and update the application state if the limit is passed.
  • A function telling the title to animate to either 1 or 0 depending on whether contentOffsetY is past the limit.
import { Override, Data, motionValue, useTransform } from "framer"
// Keep track of the state of our application
const data = Data({ isPastLimit: false })
// Create a MotionValue to track contentOffsetY
const contentOffsetY = motionValue(0)
// Listen for changes to contentOffsetY
contentOffsetY.onChange(offset => (data.isPastLimit = offset < -52))
// Apply this override to your scroll component
export function TrackScroll(): Override {
return { contentOffsetY: contentOffsetY }
}
// Apply this override to a frame containing your title
export function ShowTitleIfPastLimit(): Override {
return {
opacity: 0, // set it to zero initially
animate: data.isPastLimit ? { opacity: 1 } : { opacity: 0 },
}
}
COPY

One important thing to note. Framer doesn’t allow you to override the opacity of a text layer. So you will need to wrap a frame around the text layer and apply to override to the frame instead. Get used to seeing this:

property = condition ? valueWhenTrue : valueWhenFalse
COPY

In React, this is your best friend.

Scroll smoothly to a fixed position

Let’s say our prototype has a button to automatically scroll content back to it’s starting position. To implement this, we need the useAnimation function which returns an animation controller with start and stop methods.

The start method is explained in the API docs. It takes an argument with the property and value we want to animate to. In our case, we are animating the y translation of the scroll content layer directly.

onTap: () => controls.start({ y: 0 })
COPY

So the code has three parts:

  • Create the animation controller
  • Override the scroll component’s scrollAnimate property with this controller
  • Override the button’s onTap property with a function to start the animation

Here is the code in full:

import { Override, useAnimation } from "framer"
// Declare the controls variable so we can access it from both functions
let controls
// Apply this override to your scroll component
export function Scroll(): Override {
controls = useAnimation()
return { scrollAnimate: controls }
}
// Apply this override to your buton
export function Button(): Override {
return {
onTap: () => controls.start({ y: 0 }),
}
}
COPY

Sticky Headers

I’m going to end by showing you how to add Sticky Headers to your prototypes. The code is almost identical to the solution for parallax, we just need to apply a different transformation.

If the header travels 400px before it sticks, then we need the following transform:

const y = useTransform(contentOffsetY, [0, -400, -800], [0, 0, 400], {
clamp: false,
})
COPY

Here’s the code in full:

import { Override, motionValue, useTransform } from "framer"
// Create a MotionValue to track scroll content offset
const contentOffsetY = motionValue(0)
// Apply this override to your scroll component
export function TrackScroll(): Override {
return {
contentOffsetY: contentOffsetY,
}
}
// Apply this override to your header component
export function StickyHeader(): Override {
const travel = 400
const y = useTransform(
contentOffsetY,
[0, -travel, -travel * 2],
[0, 0, travel],
{
clamp: false,
}
)
return {
y: y,
}
}
COPY

There’s a better way!

This approach works fine when you have multiple section headers, where each one sticks once it reaches the top, pushing the previous header out the way. But, depending on your layout, you might need to calculate and apply a different travel value for each one.

This sounds hard, but don’t panic! I’ve made a Framer Store package to save you the effort.

After you install this package you will see two components: StickyScroll and StickyElement.

StickyScroll works like a native scroll component. StickyElement should be placed inside your scroll content and connected to another canvas element. This will make the connected content appear inside the scroll content and stick when you scroll past the top of the scroll component.

You can use multiple StickyElements in your scroll content. When each StickyElement reaches the top, it pushes the StickyElement above it up and off the top of the scroll component.

Time to put it all together

Congratulations! You’ve mastered scrolling. With the techniques and concepts you’ve learned here, you have the building blocks of almost every kind of scroll interaction.

Here are some other helpful resources to advance your Framer X knowledge: