DEV Community

Cover image for Smooth Scrolling with Locomotive Scroll, Astro & React
Alejandro Londoño
Alejandro Londoño

Posted on • Edited on

Smooth Scrolling with Locomotive Scroll, Astro & 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!

Imagine monitoring actually built for developers

Billboard image

Join Vercel, CrowdStrike, and thousands of other teams that trust Checkly to streamline monitor creation and configuration with Monitoring as Code.

Start Monitoring

Top comments (0)

Cloudinary image

Zoom pan, gen fill, restore, overlay, upscale, crop, resize...

Chain advanced transformations through a set of image and video APIs while optimizing assets by 90%.

Explore

👋 Kindness is contagious

Dive into an ocean of knowledge with this thought-provoking post, revered deeply within the supportive DEV Community. Developers of all levels are welcome to join and enhance our collective intelligence.

Saying a simple "thank you" can brighten someone's day. Share your gratitude in the comments below!

On DEV, sharing ideas eases our path and fortifies our community connections. Found this helpful? Sending a quick thanks to the author can be profoundly valued.

Okay