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.
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
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:
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:
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:
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 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:
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:
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:
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.
We need to increase
setInterval to regularly update
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:
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
Finally, we need to animate the progress bar’s width as time elapses. We have to use Framer’s
transform function instead of
elapsedMs is a regular number and not a MotionValue. The idea is the same:
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,
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.