DEV Community

Cover image for Professional Smooth Scroll with Locomotive-Scroll, Astro, and React
Alejandro Londoño
Alejandro Londoño

Posted on

Professional Smooth Scroll with Locomotive-Scroll, Astro, and React

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.

alogocode.site

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
import LocomotiveScroll from 'locomotive-scroll';

const scroll = new LocomotiveScroll({
    el: document.querySelector('[data-scroll-container]'),
    smooth: true
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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);
    // ...
Enter fullscreen mode Exit fullscreen mode

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);
    };
}, []);
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

That’s all! I hope this article was helpful ♥. See you next time!

Top comments (0)