Floating parallax images with React and framer-motion
In this article I’ll go over how to achieve the following technique using React and framer-motion to give your content a floaty/parallaxy feel that can help add a little flair to your layouts.
The first thing we’ll want to do our is create our ParallaxItem component and import the correct libraries.
Doesn’t do much yet. We’ll want our component to take children
and className
props for customization.
The next thing we’ll want to do is use framer-motion’s useViewportScroll
so that we can hook into the scroll event and grab the scrollY
value. We will use this to compute relative parallax values for each item.
Now that we have access to the window’s scrollY
value, lets use another useful hook that framer-motion provides: useTransform
.
useTransform
takes a value and maps it onto another value. Let’s add that and then use the motion.div
component to plug in our spring
.
The useTransform
here is saying:
“When scrollY
falls between 0–500 return a value within the range of 0%-100%. So if scrollY
were 250 the output value would be 50%.”
Great, but a scroll range of 0–500 will only work when you’ve scrolled between 0 to 500 pixels. We want to make this value relative to our individual items so we can place them anywhere on the page.
To do this, we need to get the offsetTop
of our element, and then plug that value into our transform function. Let’s create an offsetTop
state to do this.
So this still doesn’t work yet, we need to add a buffer zone to our range so that the effect is visible. Let’s set an arbitrary value of 500
:
Now our code in the transform here is saying when scrollY
is equal to the offsetTop
minus 500 pixels, return 0%, and when it’s equal to offsetTop
plus 500 pixels, return 100%. Now we should start seeing the effect, but it’s quite strong because our output range is 0 to 100%. Lets adjust that a bit.
Hopefully its working and looking better now. But you might notice that it clips content when the parallax effect is at the maximum value. That’s not really ideal. We can combat this by setting a min-height to our item that accounts for the initial height + any space that it needs to move around. Let’s add a function to calculate that and call it in our previous useLayoutEffect
In this code we also created a new variable range
and converted it to a decimal instead of a string of 20%.
Now the content should stop clipping and your parallax items have a nice amount of breathing room with which to move around.
Now let’s adjust the spring values to make them feel smoother, and give add a little element of randomness.
By randomizing the mass
, elements should move a little faster or a little slower instead of all moving together in lockstep, which helps it to feel more organic.
The final step is making sure it’s responsive. Since we only calculate the offsetTop
and minHeight
whenever the ref changes, resizing the window could cause overlapping content or tall blocks of empty space. Let’s add a window resize listener in our useLayoutEffect
to fix that:
Take a look at index.js
in the code sandbox above to see the final code which is organized a little better and get a feel for how it works in practice.
Caveats
If the content you’re using inside of your ParallaxItem component does not have a defined height by the time it runs setMinHeight
a race condition will most likely cause the min-height to remain as the default value of ‘auto’, meaning your content will clip. This can happen when using images, lazy loaded or otherwise. A good solution for this is to use aspect ratio sizing using padding-bottom. Here’s a good writeup: https://css-tricks.com/aspect-ratio-boxes/. You can see an example in action in the code sandbox at the top of this article.