Recreating Spotify’s Scroll Animation in Framer X

Spotify’s scroll animation is a textbook example of how to use motion to draw users into a page. A few months back, I recreated the effect in Framer X. Here’s how to do it.

Download the commented source file

1. Designing the interface

We first need to create the layout using design components. The page is image-heavy. We can fill in all the cover art using the Unsplash Images package in the Store. The package Icon Generator gives us easy access to Feather Icons. The most important component is the Scroll component. Our goal is to animate the design components as the user scrolls.

2. Keeping track of the scroll distance

We need to keep track of the scroll distance or how much the user has scrolled. We can make a variable to hold the scroll distance in pixels (integer) and make the Scroll component update it. Let’s call this variable scrollDistance.

The scroll distance will change constantly. The Scroll component updates it even when the user scrolls only a single pixel. We can let Framer know scrollDistance is a rapidly changing measure of motion by using a MotionValue instead of a plain number:

https://gist.github.com/vincentmvdm/f07f3109ce8be3f47c89f0eef326c426

Framer components like our Scroll component know how to update MotionValues. We just need to provide our Scroll component with our MotionValue! Create an Override that returns the MotionValue as a prop called contentOffsetY and apply it to the Scroll component:

https://gist.github.com/vincentmvdm/898e454de2d67f655670daa5955cd3f6

The Scroll component now receives our MotionValue as a prop. Scroll components update any MotionValue passed to them as contentOffsetY when the user scrolls.

3. Animating elements on scroll

We can use the scroll distance to animate our design components. Many elements change as the Spotify user scrolls. Let’s focus on the artist name (“Framerista”) for now.

The artist name fades out on scroll. We know that we need to override its opacity. Make the following Override and apply it to the design component:

https://gist.github.com/vincentmvdm/3e59e71ebe646c2d8f77c263e097fa25

The artist name now uses our Override’s opacity property. But the opacity is always 1. We need to modify our Override’s opacity every time the scroll distance changes. Spotify fades out the artist name when the user scrolls down. It’s faded out completely by the time the user has scrolled 224px. We need to change the artist name’s opacity from 1 to 0 as our scrollDistance MotionValue changes from 0 to -224px. The scroll distance is negative because the content moves up when the user scrolls down.

We have a MotionValue (scroll distance) in one range and need to transform it into a MotionValue in another range (opacity). We can do this using the useTransform function. useTransform takes four arguments. The first argument is the input or MotionValue we want to transform. The second argument is the input value’s range. The third argument is the output value’s range. The fourth argument is an optional options object. Framer stops transforming the input when it’s outside of the provided range. The opacity remains 0 if the user keeps scrolling. Animating the artist name’s opacity from 1 to 0 as the user scrolls down 224px looks like this:

https://gist.github.com/vincentmvdm/635f874e3062b87069e026a6a9f7b830

We can add as many scroll animations as we’d like now that we have the basics down. For example, we can decrease the text size:

https://gist.github.com/vincentmvdm/5d415b9bd1dfbb3813c1e34a6b30cce8

4. Polishing our animations

Our design changes as the user scrolls but our animations feel dull and unnatural. They are linear: the opacity always changes at the same rate in the previous example. That isn’t very exciting nor how objects behave in the real world. How do we get closer to the animation we’re trying to imitate?

We can make our animations feel more lively by specifying easing. The NPM package @popmotion/easing has easing functions for common animation curves like cubic-bezier. We can install this package by opening our project folder in Terminal and running the command npm install @popmotion/easing. You can locate your project folder by going to File -> Show Project Folder.

useTransform lets us specify the animation’s easing as part of its fourth argument or options object:

https://gist.github.com/vincentmvdm/956307658c12eeb20793b8f0970ba7be

Mimicking Spotify leads to five Overrides like this that change a design component’s appearance when the user scrolls.

5. Making it stick

One of the harder scroll animations is the pause button that becomes sticky past a certain distance. We could use the same technique as we have used so far. However, the arguments we need to pass into useTransform become less straightforward.

It’s easier to grab Giles Perry’s Sticky Headers package from the Store. Swap out the Scroll component for the StickyScroll component. Connect the StickyElement component to the button. Lastly, make sure the StickyElement component is inside of the StickyScroll component. All done!

6. Please don’t stop the music

Our scroll animation now looks identical to Spotify’s. But what if we wanted to use it in user testing? Our prototype needs to look real and appear to play a song. Luckily, we can animate the progress bar’s width.

First, we need to keep track of the elapsed time. In Framer X, we use Data objects when we want to store values (that aren’t MotionValues) for use in Overrides. Data objects look and behave like regular JavaScript objects. Framer updates an Override when it uses a value stored in a Data object and this value changes:

https://gist.github.com/vincentmvdm/6be1e188a2936fc86b7a97afeabff3b4

We need to increase elapsedMs as time passes. Ideally, we update it frequently enough that our progress bar animates smoothly. We could use the built-in JavaScript function setInterval to regularly update elapsedMs. However, setInterval becomes unreliable when the browser is busy. We run the risk that it won’t update elapsedMs at 60 fps. The result is a choppy animation.

But all hope isn’t lost! We can use the browser’s requestAnimationFrame function. The following code runs the timer function 60 times per second:

https://gist.github.com/vincentmvdm/800fe3c8635d9bfc11456ebbde380e33

The browser automatically passes the current time in milliseconds as an argument to our function. We can use this to calculate how much time has passed and update elapsedMs:

https://gist.github.com/vincentmvdm/11765d22d310993fa78801dec18faef7

Finally, we need to animate the progress bar’s width as time elapses. We have to use Framer’s transform function instead of useTransform because elapsedMs is a regular number and not a MotionValue. The idea is the same:

https://gist.github.com/vincentmvdm/5c5fbc560bc87d3f5ff85669be6f68ee

That’s it! The cool thing about knowing how to work with MotionValues and useTransform is that we can create other scroll effects, such as parallax, as well as most animations in today’s popular apps.

Until next time,

Vincent

Thanks to Parker Henderson for reviewing the source file and Giles Perry for his wonderful article on scrolling in Framer X.

Questions? Send me a tweet.

Designer @facebook

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store