For 2025, I decided to redesign my website, and for this task, I wanted to implement a couple of features that may seem minor at first glance but truly add a lot of quality to web pages and take the user experience to the next level.
The specific feature is smooth scrolling, and thanks to the locomotive-scroll library, it greatly simplifies the work and allows us to give a professional finish to our site.
I’m writing this article because I faced a few issues when implementing this library with the stack I was using to build my website, which is made with Astro and React. So, I’m sharing my solution in hopes it may help someone else someday.
Stack
The website is built with the following technologies, although for this case, we will focus entirely on locomotive-scroll:
Repo: alogocode.site
- Astro
- TypeScript
- React
- Tailwind CSS
- React Icons
- shadcn/ui
- Prettier
- Locomotive-scroll
Installation
npm install locomotive-scroll
Usage
According to the official documentation (GitHub), the usage would be as follows:
Smooth
With smooth scrolling and parallax.
<div data-scroll-container>
<div data-scroll-section>
<h1 data-scroll>Hey</h1>
<p data-scroll>👋</p>
</div>
<div data-scroll-section>
<h2 data-scroll data-scroll-speed="1">What's up?</h2>
<p data-scroll data-scroll-speed="2">😬</p>
</div>
</div>
import LocomotiveScroll from 'locomotive-scroll';
const scroll = new LocomotiveScroll({
el: document.querySelector('[data-scroll-container]'),
smooth: true
});
In my specific case, we need to take a few additional steps.
Implementing in Astro and React
Since we are using React, we can encapsulate everything in a component that will manage and wrap the entire page content, acting like a layout.
Here is the final result of our component:
import { useEffect, useRef, useState, type PropsWithChildren } from 'react';
import LocomotiveScroll from 'locomotive-scroll';
import 'locomotive-scroll/dist/locomotive-scroll.css';
const SmoothScroll = ({ children }: PropsWithChildren) => {
const scrollRef = useRef(null);
const [isLoaded, setIsLoaded] = useState(true);
useEffect(() => {
if (!scrollRef.current) return;
const scrollEl = scrollRef.current;
const scroll = new LocomotiveScroll({
el: scrollEl,
smooth: true,
lerp: 0.1,
multiplier: 1,
repeat: true,
});
const timeoutId = setTimeout(() => {
scroll.update();
setIsLoaded(false);
}, 1000);
return () => {
scroll.destroy();
clearTimeout(timeoutId);
};
}, []);
return (
<div>
{/* Loading screen */}
{isLoaded && (
<div className="fixed top-0 left-0 w-full h-full bg-slate-50 dark:bg-slate-950 z-50 flex items-center justify-center">
<span className="ml-2">Loading...</span>
</div>
)}
{/* Main content */}
<div data-scroll-container ref={scrollRef}>
{children}
</div>
</div>
);
};
export default SmoothScroll;
Explanation
We import all the necessary elements, including the locomotive-scroll CSS file:
import { useEffect, useRef, useState, type PropsWithChildren } from 'react';
import LocomotiveScroll from 'locomotive-scroll';
import 'locomotive-scroll/dist/locomotive-scroll.css';
Since we are using TypeScript, we import the PropsWithChildren
type to avoid creating a custom interface for the component's props.
We use useRef
to get the container element where the scroll properties will be applied. The useState
is used to implement a loading screen because initially, it doesn't handle large amounts of content well, so we need to update it after the page loads.
const SmoothScroll = ({ children }: PropsWithChildren) => {
const scrollRef = useRef(null);
const [isLoaded, setIsLoaded] = useState(true);
// ...
The useEffect
hook contains all the logic. First, we check if the ref is not null, then we create a new LocomotiveScroll
instance and pass the container element to the el
property.
As mentioned earlier, since the scroll doesn't capture all content properly at first, we need to update it. To avoid an awkward jump on the page, we implement a loading screen that disappears after one second using isLoaded
and setTimeout
.
Lastly, we clean up the scroll and timeout with their respective methods to avoid overloading the app.
useEffect(() => {
if (!scrollRef.current) return;
const scrollEl = scrollRef.current;
const scroll = new LocomotiveScroll({
el: scrollEl, // Scroll container element.
smooth: true,
lerp: 0.1, // This defines the "smoothness" intensity
multiplier: 1, // Factor applied to the scroll delta, allowing to boost/reduce scrolling speed
repeat: true, // Repeat in-view detection.
});
const timeoutId = setTimeout(() => {
scroll.update();
setIsLoaded(false);
}, 1000);
return () => {
scroll.destroy();
clearTimeout(timeoutId);
};
}, []);
The rendering logic is simple: the loading screen will display as long as isLoaded
is true, acting as a curtain. It won’t conflict with other elements thanks to the fixed
positioning.
return (
<div>
{/* Loading screen */}
{isLoaded && (
<div className="fixed top-0 left-0 w-full h-full bg-slate-50 dark:bg-slate-950 z-50 flex items-center justify-center">
<span className="ml-2">Loading...</span>
</div>
)}
{/* Main content */}
<div data-scroll-container ref={scrollRef}>
{children}
</div>
</div>
);
The only thing left is to use the component. Since we are using Astro and locomotive-scroll relies on browser APIs, we need to add the client:only
directive to the component:
<SmoothScroll client:only>
{/* Your content here */}
</SmoothScroll>
That’s all! I hope this article was helpful ♥. See you next time!
Top comments (0)