Want to build websites in no-time? Announcing the new Framer Beta.Learn more
Framer
Blog
  • Latest
  • Product
  • Editorial
  • Customers
  • Guides
Want to build websites in no-time? Announcing the new Framer Beta.Learn more
Framer
  • Teams
  • Showcase
  • Developers
  • Resources
    • Tutorials
    • Examples
    • Components
    • Templates
    • Sessions
    • Support
    • Updates
  • Blog
  • Pricing
  • ···
    • Blog
    • Pricing
Blog
  • Latest
  • Product
  • Editorial
  • Customers
  • Guides

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.

Giles Perry

GP

by Giles Perry

Giles Perry

GP

Twitter

Giles Perry

Principal Product Designer at Skyscanner. Giles is a product designer based in London and currently working at Skyscanner.

Learn about mastering different types of scrolling techniques
TwitterFacebookLinkedIn

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.

Interactive design that feels real

Start prototyping for free with Framer today.

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

Example file: Parallax effect

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 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,
}
}


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.

Example file: app bar effect

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 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,
}
}


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:

  1. A data object to keep track of the state of our application: true or false, is contentOffsetY past a limit?
  2. Something to listen for changes to contentOffsetY and update the application state if the limit is passed.
  3. 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 },
}
}

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:

  1. We first provide it the value we want to animate, which is our contentOffsetY stored in a motionValue.
  2. The second value we provide is the value we want to animate to, which is -570px.
  3. 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 them
let contentOffsetY
// Apply this override to your scroll component
export function Scroll(): Override {
contentOffsetY = useMotionValue(0)
return { contentOffsetY }
}
// Smooth scroll to a value
export function SmoothScroll(): Override {
return {
onTap: () => {
animate(contentOffsetY, -570, { duration: 1.5, ease: "easeOut" })
},
}
}

Sticky Headers

Make an element sticky at a given position

Example file: 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, }
)

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,
}
}


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:

  • Framer Guide to React
  • Framer API Documentation
  • The Designer’s Guide to Learning Code

Share this article

TwitterFacebookLinkedIn

Bring your best ideas to life

Subscribe to get fresh prototyping stories, tips, and resources delivered straight to your inbox.

Visual

Editorial

How to Code a Website in 2021: A Comprehensive Guide

November 2, 2021

Visual

Guides

What is Mobile-First Design, and How Do You Implement It?

November 2, 2021

Visual

Guides

How to Become a Web Designer in 2021

October 4, 2021

Framer

  • Teams
  • Pricing
  • Showcasenew
  • Blog
  • Developers
  • Updates

Platforms

  • Web
  • macOS
  • Windows
  • iOS
  • Android

Learn

  • Tutorials
  • Examples
  • Templates
  • Sessions

Resources

  • Components
  • Input Kit
  • State of Prototyping
  • Desktop Prototyping
  • Prototyping Tool
  • Mockup Tool
  • Wireframing Tool
  • UI/UX Design Tool
  • Graphic Design Tool
  • User Testing

About

  • Loupe
  • Community
  • Company
  • Careers
  • Legal

Support

  • Using Framer
  • Accounts
  • Contact

  • Twitter
  • Discord
  • Dribbble

Copyright © 2022 Framer B.V.

  • Security
  • Terms of Service
  • Privacy Statement