Component

Vertical Parallax

A lightweight TypeScript component for moving several layers on scroll.

Last updated: 08/08/2026

Why I built it

I wanted a parallax effect that was easy to integrate into a page, with few settings and behavior that is simple to understand.

The idea was to keep it lightweight: one container, several layers, one speed defined for each layer, then movement calculated on scroll.

Also, the effect is disabled on small screens and if JavaScript is disabled in the browser, the layers remain visible without animation.

Code and usage

The principle fits in a few steps: read the container position, calculate its offset inside the viewport, apply each layer speed, then update the translateY.

The goal was mainly to get a component that is simple to understand, easy to integrate, and lightweight enough not to justify adding an animation library.

1

Position

The component reads the container position inside the viewport.

2

Speed

Each layer applies its data-parallax-speed value.

3

Transform

The final movement is applied with a translateY.

Full TypeScript class

export class VerticalParallax {
    private container: HTMLDivElement; // Reference to the container element
    private elements: NodeListOf<HTMLElement>; // Collection of elements to apply parallax effect
    private isDisabled: boolean; // Boolean to check if the effect should be disabled

    /**
     * Constructor for creating a VerticalParallax instance.
     * @param container - The ID of the container HTMLDivElement.
     * @param selector - The selector used to find elements within the container for applying the parallax effect.
     * Initializes the parallax elements and attaches the necessary event listeners.
     */
    constructor(container: string, selector: string) {
        this.container = document.getElementById(container) as HTMLDivElement; // Initializes the main container div
        this.elements = document.querySelectorAll(selector); // Initializes the elements based on the selector
        this.isDisabled = this.checkIfDisabled(); // Determine if the effect should be disabled

        if (!this.isDisabled) {
            this.attachEvents(); // Set up event listeners for the scroll event if the effect is enabled
        }
    }

    /**
     * Checks if the effect should be disabled.
     * @returns boolean - True if the device is mobile or has a small viewport.
     */
    private checkIfDisabled(): boolean {
        const userAgent = navigator.userAgent || navigator.vendor || (window as Window & { opera?: string }).opera || "";
        const isMobile = /android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
        const isSmallScreen = window.matchMedia("(max-width: 768px)").matches;

        return isMobile || isSmallScreen;
    }

    /**
     * Attaches scroll event listeners to the window.
     * Listens for scroll events to apply the parallax effect.
     */
    private attachEvents(): void {
        window.addEventListener("scroll", () => {
            this.handleScroll();
        });
    }

    /**
     * Handles the scroll event to apply parallax effects to the elements.
     * Calculates and applies the vertical parallax effect based on the scroll position.
     */
    private handleScroll(): void {
        const containerRect = this.container.getBoundingClientRect(); // Get the container's position relative to the viewport

        // Check if the top of the container is within view
        if (containerRect.top <= 0 && containerRect.bottom >= 0) {
            this.elements.forEach((element) => {
                const speed = parseFloat(
                    element.getAttribute("data-parallax-speed") || "0.2"
                ); // Read and parse the parallax speed attribute

                const containerOffset = -containerRect.top;
                const parallaxOffset = containerOffset * speed;

                element.style.transform = `translateY(${parallaxOffset}px)`; // Apply the parallax transformation
            });
        } else if (containerRect.top > 0) {
            this.elements.forEach((element) => {
                element.style.transform = `translateY(0px)`; // Reset transformation when the container is above the viewport
            });
        }
    }

    /**
     * Refreshes the parallax effects manually, for use when significant page layout changes occur (like theme changes).
     */
    public refresh(): void {
        if (!this.isDisabled) {
            this.elements = this.container.querySelectorAll("[data-parallax-speed]");
            this.handleScroll();
        }
    }
}

Initialization

document.addEventListener("DOMContentLoaded", () => {
    new VerticalParallax("verticalParallax-container", ".verticalParallax-layer");
});

HTML structure

<div id="verticalParallax-container" class="relative overflow-hidden">
    <picture data-parallax-speed="1" class="verticalParallax-layer">
        <img src="/assets/img/layer-01.webp" alt="" loading="lazy">
    </picture>

    <picture data-parallax-speed="0.35" class="verticalParallax-layer">
        <img src="/assets/img/layer-02.webp" alt="" loading="lazy">
    </picture>
</div>