Scrolling through a website especially with a notched mouse wheel is typically jumpy and harder to navigate.
Smooth Scrolling, or spring scrolling adds an animated touch to the traditional mouse scroll.
If you've never experienced smooth scrolling before, try out the Live Demo.
Concept
Consider the viewport window. When a bunch of HTML elements combine to a size taller than the window, it overflows and can be accessed when you scroll down.
To override the default scroll, we need to wrap the content in a fixed element we can control.
Finally, we will create an invisible spacer (div) equal to the scroll height of the content. This will trigger the browser's default scroll bar.
Setup
Create a React Typescript Repl on Replit to get started.
Install Framer Motion with npm install framer-motion
.
The Component
The <SmoothScroll/>
Component will wrap all the HTML elements we want to incorporate in the Smooth Scrolling effect.
export default function SmoothScroll({
children
}: {
children: React.ReactNode;
}) {
return <></>;
}
Scroll & Spring Values
Import the following hooks from framer-motion
.
import { useScroll, useSpring, useTransform } from 'framer-motion';
In the SmoothScroll
component, destructure scrollYProgress
from the useScroll
hook.
const { scrollYProgress } = useScroll();
Next, use the useSpring
hook to apply the smooth effect to the scrollYProgress
value.
const smoothProgress = useSpring(scrollYProgress, { mass: 0.1 })
Content & Spacer
Add the motion
component to the existing import from framer-motion
.
- import { useScroll, useSpring } from 'framer-motion';
+ import { motion, useScroll, useSpring } from 'framer-motion';
Skip down to the component's return
statement. Return an empty <div>
element and a <motion.div>
element which renders the children
prop as a child.
return <>
<div style={{ height: contentHeight }} />
<motion.div
className="scrollBody"
>
{children}
</motion.div>
</>
Create a contentRef
react reference via the useRef
hook and a contentHeight
state with useState
.
import { useState, useRef } from 'react';
...
const contentRef = useRef<HTMLDivElement>(null);
const [contentHeight, setContentHeight] = useState(0);
Assign the content wrapper the contentRef
.
<motion.div
className="scrollBody"
ref={contentRef}
>
{children}
</motion.div>
Use the style
prop to make the spacer have a height of contentHeight
.
<div style={{ height: contentHeight }} />
Resize Handler
When the window gets resized, the height of the content content is likely to change. Since the contentHeight
value is a state, we will need to update it whenever the window resizes, and when the contentRef
reference updates.
Start by importing the useEffect
hook from react
.
- import { useState, useRef } from 'react';
+ import { useEffect, useState, useRef } from 'react';
Within the useEffect hook, create and call a handler to set the contentHeight
value to contentRef.current.scrollHeight
.
Add contentRef
to the dependency array.
useEffect(() => {
const handleResize = () => {
if (contentRef.current) {
setContentHeight(contentRef.current.scrollHeight)
}
}
handleResize();
}, [contentRef]);
Finally, add a resize
event listener to window
in the useEffect hook, and return a disposer function.
useEffect(() => {
...
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
}
}, [contentRef, children]);
Put it all together
Import the useTransform
hook from framer-motion
.
- import { motion, useScroll, useSpring } from 'framer-motion';
+ import { useTransform, motion, useScroll, useSpring } from 'framer-motion';
Create a constant y
and set it to the useTransform
hook with smoothProgress
as the initial value.
const y = useTransform(smoothProgress, value => {
return value;
});
Visualizing how to calculate the scroll, we will be subtracting the viewport height from the content container's scroll height, multiplying it by -1, and then by value
.
const y = useTransform(smoothProgress, value => {
return value * -(contentHeight - window.innerHeight);
});
Use the y
transformed value in the content container.
<motion.div
className="scrollBody"
style={{ y }}
ref={contentRef}
>
{children}
</motion.div>
Styles
I already fought CSS so you won't have to. Simply copy and paste the bare minimum CSS over and the scroll component will be ready to roll.
.scrollBody {
width: 100vw;
position: fixed;
top: 0;
display: flex;
flex-direction: column;
}
Complete π
That's it! All you have to do is add a bunch of HTML elements within the <SmoothScroll />
component.
Nothing better than fifty <h1>Lorem Ipsum</h1>
s in your codebase.
- Source Code: https://replit.com/@IroncladDev/FramerSmoothScroll?v=1
- Live Demo: https://framersmoothscroll.ironcladdev.repl.co
Thanks for reading. If you have any feedback or recommendations for this article, I'd love to hear it in the comments.
Let's get in touch π€
Top comments (9)
great post.
really easy to understand even for a beginner!
Appreciate it, thanks :)
Amazing postπ
Thank you!!
I'm having an issue where it loads scrolled halfway (but the scrollbar is at the top). when I start scrolling it jumps to the top. I think the y value is loaded before the full content height is loaded. any ideas how to fix this would be amazing!
Does this happen in the demo/example I made?
You can use useLayoutEffect hook instead of useEffect hook to solve the scroll position problem at render. I noticed that the scroll position is not calculating/functioning well using useEffect and after researching, useLayoutEffect is the best one to use on this scenario because it lets you measure the DOM measurement immediately before repaint. see useLayoutEffect documentation
Thank you so much for your article. it helped me a lot on setting up the SmoothScroll on my project.
One thing I would like to add on this. You can use useLayoutEffect hook instead of useEffect hook to solve the scroll position problem at render. I noticed that the scroll position is not calculating/functioning well using useEffect and after researching, useLayoutEffect is the best one to use on this scenario because it lets you measure the DOM measurement immediately before repaint. see useLayoutEffect documentation
Wow really well written. Easy to understand