Guest Post
A Primer on Creating Advanced Scroll Based Animations
Parallax, dynamic, or sticky headers. Learn how to leverage code to take your scroll interactions to the next level in Framer.
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. The code examples in this article rely on the Framer Library API, which is available in every Framer project.
Framer enables developers to create fully custom components, integrate with 3rd party tools, and leverage external code libraries. Express your ideas faster with any combination of design and code.
First, let’s talk about some theory
Behind the scenes, everything you see in Framer is a React component, including the Scroll tool. 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, }}
All my examples use overrides. If you are new to Framer you can read more about code overrides in the article covering Overrides.
If you need an introduction to React, you might enjoy reading Koen Bok’s Framer Guide to React.
Parallax
Parallax effects can give your prototype the illusion of differences in distances
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 in 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
The 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)
Then use it to override contentOffsetY
in our scroll component:
export function TrackScroll(): Override { return { contentOffsetY: contentOffsetY }}
Whenever we scroll, the MotionValue
will update with the content offset value.
Tip: The Scroll component comes with a useful method to execute a function each time we scroll. This can be useful to log something to the console upon scrolling:
export function TrackScroll(): Override {
return { contentOffsetY: contentOffsetY, onScroll: (event: any) => { console.log(contentOffsetY.current) }, }}
Transforming MotionValues
The Framer Library also gives us a way to transform MotionValues
with the useTransform
function. useTransform
creates a MotionValue
by transforming the output of another MotionValue
.
For example:
const y = useTransform(contentOffsetY, [0, -100], [0, 50])
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}
.
And here’s the complete code:
import { Override, motionValue, useTransform } from "framer"
const contentOffsetY = motionValue(0)
// Apply this override to your scroll componentexport function TrackScroll(): Override { return { contentOffsetY: contentOffsetY }}
// Apply this override to your parallax layerexport function ParallaxLayer(): Override { const y = useTransform(contentOffsetY, [0, -100], [0, 50], { clamp: false, }) return { y: y, }}
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})
Dynamic app bar
Scrolling can be used to change how a navigation bar looks at different points.
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, }}
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})
Putting it all together:
import { Override, motionValue, useTransform } from "framer"
const contentOffsetY = motionValue(0)
// Apply this override to your scroll componentexport function TrackScroll(): Override { return { contentOffsetY: contentOffsetY }}
// Apply this override to your app barexport function AppBar(): Override { const height = useTransform(contentOffsetY, [0, -52, -52], [140, 88, 88], { clamp: false, })
return { height: height, }}
Animate on scrolling at set positions
Example file: triggering an animation at a scroll position
Another feature of app bars in iOS is the 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 contentOffsetY
passes a threshold?
This is a good time for a bit more theory
If you’re familiar with Framer Classic, you will discover a whole new programming model when working in React.
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 different 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
orfalse
, iscontentOffsetY
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
or0
depending on whethercontentOffsetY
is past the limit.
import { Override, Data, motionValue, useTransform } from "framer"
// Keep track of the state of our applicationconst data = Data({ isPastLimit: false })
// Create a MotionValue to track contentOffsetYconst contentOffsetY = motionValue(0)
// Listen for changes to contentOffsetYcontentOffsetY.onChange(offset => (data.isPastLimit = offset < -52))
// Apply this override to your scroll componentexport function TrackScroll(): Override { return { contentOffsetY: contentOffsetY }}
// Apply this override to a frame containing your titleexport function ShowTitleIfPastLimit(): Override { return { opacity: 0, // set it to zero initially animate: data.isPastLimit ? { opacity: 1 } : { opacity: 0 }, }}
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
In React, this is your best friend.
Scroll smoothly to a fixed position
Scroll to a fixed position after an event
Example file: scroll smoothly to a fixed position
Let’s say our prototype has a button to automatically scroll content back to its starting position. To implement this, we need the animate function.
Before we start, declare a variable that we will use to store the value of our offset outside any other function:
let contentOffsetY
Now create a new override that we will apply to our button. On this override, we start by specifying an onTap event that executes another function.
In this function, we specify the animate function with various arguments:
- We first provide it the value we want to animate, which is our
contentOffsetY
stored in amotionValue
. - The second value we provide is the value we want to animate to, which is
-570px
. - Our third value is optional and allows us to set a custom transition. We went with an ease-out easing of 1.5 seconds.
export function SmoothScroll(): Override { return { onTap: () => { animate(contentOffsetY, -570, { duration: 1.5, ease: "easeOut" }) }, }}
As with the other examples, we’ll have another override on top of our Scroll component that only listens to any changes to contentOffsetY
.
export function Scroll(): Override { contentOffsetY = useMotionValue(0)
return { contentOffsetY }}
Here is the code in full:
import { Override, useMotionValue, animate } from "framer"
// Declare the controls variable outside the other functions so we can access it from all of themlet contentOffsetY
// Apply this override to your scroll componentexport function Scroll(): Override { contentOffsetY = useMotionValue(0)
return { contentOffsetY }}
// Smooth scroll to a valueexport function SmoothScroll(): Override { return { onTap: () => { animate(contentOffsetY, -570, { duration: 1.5, ease: "easeOut" }) }, }}
Sticky Headers
Make an element sticky at a given position
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, })
Here’s the code in full:
import { Override, motionValue, useTransform } from "framer"
// Create a MotionValue to track scroll content offsetconst contentOffsetY = motionValue(0)
// Apply this override to your scroll componentexport function TrackScroll(): Override { return { contentOffsetY: contentOffsetY, }}
// Apply this override to your header componentexport function StickyHeader(): Override { const travel = 400 const y = useTransform( contentOffsetY, [0, -travel, -travel * 2], [0, 0, travel], { clamp: false, } ) return { y: y, }}
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 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 knowledge:
Bring your best ideas to life
Subscribe to get fresh prototyping stories, tips, and resources delivered straight to your inbox.