Back to Gallery f04

Parallax Hero

FREE

A layered hero section where background blobs, headline, and foreground labels scroll at different speeds, creating scrubbed parallax depth.

ScrollTrigger parallaxscrubscroll-reveal Lenis beginner

About This Effect

A classic layered parallax hero driven by a single scrubbed ScrollTrigger. Each layer declares its own speed with a data attribute: slow gradient blobs in the background, a big Syne headline in the middle, and a fast label strip in the foreground. The speed difference between layers creates a convincing sense of depth as the hero scrolls through the viewport, forwards and backwards.

What's Included

  • Data-attribute API: drop data-parallax on a container and data-parallax-speed on each layer
  • One scrubbed ScrollTrigger per container, so any number of layers costs a single trigger
  • Speed values below 1 lag behind the scroll, values above 1 race ahead for foreground depth
  • Optional data-parallax-fade attribute fades a layer out as the hero leaves the viewport
  • GPU-friendly: yPercent transforms only, with force3D and will-change on every layer
  • clamp() positioning prevents layer jumps when the hero is already in view on load
  • Works with any markup: text, gradient divs, images, or video layers
  • Full reduced motion support: layers render static with no blank content

Perfect For

  • Landing page heroes with instant visual depth and no image assets
  • Portfolio and agency intros with layered typography that separates on scroll
  • Product launch pages with foreground badges that race ahead of the scroll
  • Editorial section headers with slow-drifting background shapes
  • Marketing sites that need a premium scroll feel from a beginner-friendly snippet

How It Works

Each container with data-parallax gets one GSAP timeline bound to a ScrollTrigger with scrub: true, running from clamp(top bottom) to clamp(bottom top). Layers are collected with gsap.utils.toArray and each receives a fromTo tween on yPercent proportional to (1 - speed), so slower layers drift down relative to the scroll while faster layers pull ahead. Layers marked data-parallax-fade get an extra autoAlpha tween positioned in the final stretch of the timeline, and a gsap.matchMedia reduce branch pins everything static for users who prefer reduced motion.

Plugins ScrollTrigger
Difficulty Beginner
Smooth Scroll Lenis Integration
Includes HTML + JS + CSS source, documentation, lifetime updates
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="robots" content="noindex, nofollow">
    <title>Parallax Hero Demo | GSAP Vault</title>
    <link rel="stylesheet" href="assets/style.css">
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&family=Syne:wght@400;500;600;700;800&display=swap" rel="stylesheet">
</head>
<body>
    <main class="container">
        <header class="page-header">
            <h1 class="page-title">Parallax Hero</h1>
            <p class="page-subtitle">A layered hero where every element scrolls at its own speed, scrubbed by ScrollTrigger for real depth</p>
        </header>

        <!-- Example 1: Layered Hero -->
        <section class="example">
            <div class="example-header">
                <span class="example-number">01</span>
                <h2 class="example-title">Layered Hero</h2>
            </div>
            <div class="example-content">
                <p class="example-instruction">Scroll through the hero. The gradient blobs drift slowly in the background, the headline moves at mid speed and fades as it leaves, and the label strip races ahead in the foreground:</p>

                <section class="parallax-hero" data-parallax aria-label="Layered parallax hero">
                    <div class="parallax-layer" data-parallax-speed="0.2" aria-hidden="true">
                        <div class="hero-blob hero-blob--violet"></div>
                        <div class="hero-blob hero-blob--magenta"></div>
                    </div>
                    <div class="parallax-layer" data-parallax-speed="0.45" aria-hidden="true">
                        <div class="hero-blob hero-blob--indigo"></div>
                        <div class="hero-grid"></div>
                    </div>
                    <div class="parallax-layer parallax-layer--title" data-parallax-speed="0.7" data-parallax-fade>
                        <p class="hero-kicker">GSAP Vault</p>
                        <h2 class="hero-title">Depth<br><span>In Motion</span></h2>
                    </div>
                    <div class="parallax-layer parallax-layer--strip" data-parallax-speed="1.3">
                        <div class="hero-strip">
                            <span>Scrubbed</span>
                            <span>ScrollTrigger</span>
                            <span>Layered</span>
                            <span>Parallax</span>
                        </div>
                    </div>
                </section>
            </div>
        </section>

        <!-- Example 2: Scroll Room / Context -->
        <section class="example">
            <div class="example-header">
                <span class="example-number">02</span>
                <h2 class="example-title">After The Hero</h2>
            </div>
            <div class="example-content">
                <p class="example-instruction">Scroll back up and watch the layers separate again. The effect is fully scrubbed, so it plays forwards and backwards with the scrollbar:</p>

                <div class="after-hero">
                    <p class="label">How it reads</p>
                    <h3 class="after-hero-title">Three speeds, one scrollbar</h3>
                    <p class="after-hero-copy">The background blobs use a speed of 0.2, so they barely move while the page scrolls past them. The headline sits at 0.7, close to normal scroll speed, and fades out as the hero leaves the viewport. The label strip uses 1.3, moving faster than the scroll itself, which pushes it into the foreground.</p>
                    <p class="after-hero-copy">Each layer is a single scrubbed tween on <code>yPercent</code>, so the whole hero costs one ScrollTrigger and a handful of GPU-accelerated transforms.</p>
                </div>
            </div>
        </section>
    </main>

    <!-- GSAP Core -->
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/ScrollTrigger.min.js"></script>

    <!-- Lenis smooth scroll -->
    <script src="https://unpkg.com/lenis@1.3.17/dist/lenis.min.js"></script>

    <!-- Effect code -->
    <script src="assets/script.js"></script>
</body>
</html>
/**
 * Parallax Hero
 *
 * Layered hero section where elements move at different speeds while
 * scrolling. One scrubbed ScrollTrigger per container maps each layer's
 * yPercent to its data-parallax-speed, creating a sense of depth.
 *
 * @plugins ScrollTrigger
 * @techniques parallax, scrub, scroll-reveal
 */

gsap.registerPlugin(ScrollTrigger);

window.addEventListener('DOMContentLoaded', function initParallaxHero() {
    // ============================================
    // OPTIONAL: Lenis smooth scroll integration
    // Remove this block if you are not using Lenis
    // ============================================
    let lenis = null;
    if (typeof Lenis !== 'undefined') {
        lenis = new Lenis({
            duration: 1.2,
            smoothWheel: true
        });

        lenis.on('scroll', ScrollTrigger.update);

        gsap.ticker.add(function lenisRaf(time) {
            lenis.raf(time * 1000);
        });
        gsap.ticker.lagSmoothing(0);

        window.lenis = lenis;
    }

    const ctx = gsap.context(function gsapContextCallback() {
        const mm = gsap.matchMedia();

        // ============================================
        // MOTION: scrubbed parallax layers
        // ============================================
        mm.add('(prefers-reduced-motion: no-preference)', function motionBranch() {
            const containers = gsap.utils.toArray('[data-parallax]');

            containers.forEach(function initContainer(container) {
                const layers = gsap.utils.toArray('[data-parallax-speed]', container);
                if (!layers.length) return;

                // One scrubbed timeline per container. Timeline progress maps
                // to the container's full journey through the viewport.
                // clamp() prevents a visual jump when the container is already
                // partially in view on page load.
                const tl = gsap.timeline({
                    defaults: {
                        ease: 'none',
                        duration: 1,
                        force3D: true
                    },
                    scrollTrigger: {
                        trigger: container,
                        start: 'clamp(top bottom)',
                        end: 'clamp(bottom top)',
                        scrub: true,
                        invalidateOnRefresh: true
                    }
                });

                layers.forEach(function initLayer(layer) {
                    const parsed = parseFloat(layer.dataset.parallaxSpeed);
                    const speed = isNaN(parsed) ? 1 : parsed;

                    // speed 1 tracks the scroll exactly (no shift).
                    // speed < 1 lags behind (background depth).
                    // speed > 1 races ahead (foreground depth).
                    const shift = (1 - speed) * 50;

                    tl.fromTo(layer,
                        { yPercent: -shift },
                        { yPercent: shift },
                        0
                    );

                    // Optional: fade the layer out as the container leaves
                    // the top of the viewport
                    if (layer.hasAttribute('data-parallax-fade')) {
                        tl.fromTo(layer,
                            { autoAlpha: 1 },
                            { autoAlpha: 0, duration: 0.45 },
                            0.55
                        );
                    }
                });
            });

            return function cleanup() {
                ScrollTrigger.getAll().forEach(function killTrigger(trigger) {
                    trigger.kill();
                });
            };
        });

        // ============================================
        // REDUCED MOTION: layers stay static
        // ============================================
        mm.add('(prefers-reduced-motion: reduce)', function reducedBranch() {
            gsap.set('[data-parallax] [data-parallax-speed]', {
                yPercent: 0,
                autoAlpha: 1
            });
        });
    });

    // Store context for SPA cleanup
    window.gsapContext = ctx;

    window.addEventListener('beforeunload', function teardown() {
        if (ctx) ctx.kill();
        if (lenis) lenis.destroy();
    });
});
gsap.registerPlugin(ScrollTrigger),window.addEventListener("DOMContentLoaded",function(){let e=null;"undefined"!=typeof Lenis&&(e=new Lenis({duration:1.2,smoothWheel:!0}),e.on("scroll",ScrollTrigger.update),gsap.ticker.add(function(a){e.raf(1e3*a)}),gsap.ticker.lagSmoothing(0),window.lenis=e);const a=gsap.context(function(){const e=gsap.matchMedia();e.add("(prefers-reduced-motion: no-preference)",function(){return gsap.utils.toArray("[data-parallax]").forEach(function(e){const a=gsap.utils.toArray("[data-parallax-speed]",e);if(!a.length)return;const t=gsap.timeline({defaults:{ease:"none",duration:1,force3D:!0},scrollTrigger:{trigger:e,start:"clamp(top bottom)",end:"clamp(bottom top)",scrub:!0,invalidateOnRefresh:!0}});a.forEach(function(e){const a=parseFloat(e.dataset.parallaxSpeed),n=50*(1-(isNaN(a)?1:a));t.fromTo(e,{yPercent:-n},{yPercent:n},0),e.hasAttribute("data-parallax-fade")&&t.fromTo(e,{autoAlpha:1},{autoAlpha:0,duration:.45},.55)})}),function(){ScrollTrigger.getAll().forEach(function(e){e.kill()})}}),e.add("(prefers-reduced-motion: reduce)",function(){gsap.set("[data-parallax] [data-parallax-speed]",{yPercent:0,autoAlpha:1})})});window.gsapContext=a,window.addEventListener("beforeunload",function(){a&&a.kill(),e&&e.destroy()})});
/* ============================================
   GSAP VAULT DESIGN SYSTEM
   ============================================ */

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

:root {
    /* Core colors */
    --black: #0a0a0a;
    --dark: #111111;
    --surface: #1a1a1a;
    --surface-light: #242424;
    --border: #2a2a2a;
    --border-light: #3a3a3a;

    /* Text hierarchy */
    --text: #ffffff;
    --text-secondary: #b0b0b0;
    --text-muted: #999999;

    /* Accent colors - CHOOSE ONE per effect */
    --lime: #c8ff00;        /* Energetic, techy */
    --lime-dim: #a8d900;
    --purple: #a855f7;      /* Creative, premium */
    --purple-light: #c084fc;
    --orange: #ff6b35;      /* Warm, playful */
    --orange-light: #ff8c5a;
    --cyan: #22d3ee;        /* Cool, modern */
    --pink: #ff3366;        /* Bold, vibrant */

    /* Set chosen accent (change per effect) */
    --accent: var(--purple);

    /* Easing - matches GSAP patterns */
    --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
    --ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
    --ease-out-cubic: cubic-bezier(0.33, 1, 0.68, 1);
}

html {
    scroll-behavior: auto;
}

body {
    background: var(--black);
    color: var(--text);
    font-family: 'Space Grotesk', system-ui, sans-serif;
    font-weight: 400;
    overflow-x: hidden;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    line-height: 1.6;
}

::selection {
    background: var(--accent);
    color: var(--black);
}

/* ============================================
   CUSTOM SCROLLBAR
   ============================================ */
::-webkit-scrollbar {
    width: 10px;
}

::-webkit-scrollbar-track {
    background: var(--black);
}

::-webkit-scrollbar-thumb {
    background: var(--surface-light);
    border-radius: 5px;
    border: 2px solid var(--black);
}

::-webkit-scrollbar-thumb:hover {
    background: var(--border-light);
}

/* ============================================
   TYPOGRAPHY
   ============================================ */

/* Syne for display/headlines */
h1, h2, h3, h4, h5, h6 {
    font-family: 'Syne', system-ui, sans-serif;
    font-weight: 700;
    line-height: 1.1;
    letter-spacing: -0.03em;
}

/* JetBrains Mono for code/labels */
code {
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.85em;
    background: var(--surface);
    padding: 0.15em 0.4em;
    border-radius: 4px;
}

/* ============================================
   LAYOUT
   ============================================ */
.container {
    max-width: 1000px;
    margin: 0 auto;
    padding: 5rem 2rem;
}

/* ============================================
   PAGE HEADER
   ============================================ */
.page-header {
    text-align: center;
    margin-bottom: 6rem;
    padding-top: 2rem;
}

.page-title {
    font-size: clamp(2.5rem, 7vw, 4rem);
    font-weight: 700;
    letter-spacing: -0.03em;
    margin-bottom: 1rem;
}

.page-subtitle {
    font-size: 1.125rem;
    color: var(--text-secondary);
    max-width: 500px;
    margin: 0 auto;
}

/* ============================================
   EXAMPLE SECTIONS
   ============================================ */
.example {
    margin-bottom: 8rem;
    padding-bottom: 4rem;
    border-bottom: 1px solid var(--border);
}

.example:last-child {
    margin-bottom: 0;
    padding-bottom: 0;
    border-bottom: none;
}

.example-header {
    display: flex;
    align-items: center;
    gap: 1rem;
    margin-bottom: 2rem;
}

.example-number {
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--accent);
    letter-spacing: 0.1em;
}

.example-title {
    font-family: 'Space Grotesk', system-ui, sans-serif;
    font-size: 0.875rem;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--text-muted);
}

.example-content {
    padding-left: 3rem;
}

.example-instruction {
    font-size: 1rem;
    color: var(--text-secondary);
    margin-bottom: 2rem;
    line-height: 1.7;
}

/* ============================================
   LABELS
   ============================================ */
.label {
    display: inline-block;
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.15em;
    color: var(--accent);
}

/* ============================================
   EFFECT-SPECIFIC STYLES: PARALLAX HERO
   ============================================ */

.parallax-hero {
    position: relative;
    height: 92vh;
    min-height: 540px;
    overflow: clip;
    border-radius: 24px;
    border: 1px solid var(--border);
    background: linear-gradient(180deg, var(--dark) 0%, var(--black) 100%);
}

/* Every layer fills the hero and is moved by GSAP */
.parallax-layer {
    position: absolute;
    inset: -12% 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    will-change: transform;
    pointer-events: none;
}

/* --- Background blobs (pure CSS, no images) --- */
.hero-blob {
    position: absolute;
    border-radius: 50%;
}

.hero-blob--violet {
    width: min(560px, 70vw);
    height: min(560px, 70vw);
    top: 2%;
    left: -8%;
    background: radial-gradient(circle at 35% 35%, rgba(168, 85, 247, 0.55) 0%, rgba(168, 85, 247, 0) 68%);
}

.hero-blob--magenta {
    width: min(480px, 62vw);
    height: min(480px, 62vw);
    bottom: -4%;
    right: -6%;
    background: radial-gradient(circle at 60% 40%, rgba(255, 51, 102, 0.35) 0%, rgba(255, 51, 102, 0) 70%);
}

.hero-blob--indigo {
    width: min(420px, 55vw);
    height: min(420px, 55vw);
    top: 24%;
    right: 14%;
    background: radial-gradient(circle at 50% 50%, rgba(99, 102, 241, 0.4) 0%, rgba(99, 102, 241, 0) 66%);
}

/* Faint dot grid for extra depth on the mid layer */
.hero-grid {
    position: absolute;
    inset: 0;
    background-image: radial-gradient(rgba(255, 255, 255, 0.08) 1px, transparent 1px);
    background-size: 34px 34px;
    mask-image: radial-gradient(ellipse at center, black 30%, transparent 75%);
    -webkit-mask-image: radial-gradient(ellipse at center, black 30%, transparent 75%);
}

/* --- Mid layer: headline --- */
.parallax-layer--title {
    gap: 1.25rem;
    text-align: center;
}

.hero-kicker {
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.3em;
    color: var(--accent);
}

.hero-title {
    font-family: 'Syne', system-ui, sans-serif;
    font-size: clamp(3.25rem, 11vw, 8rem);
    font-weight: 800;
    line-height: 0.95;
    letter-spacing: -0.04em;
    text-transform: uppercase;
}

.hero-title span {
    background: linear-gradient(90deg, var(--purple-light), var(--purple));
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

/* --- Foreground layer: label strip --- */
.parallax-layer--strip {
    justify-content: flex-end;
    padding-bottom: 16%;
}

.hero-strip {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.75rem 2.5rem;
    padding: 0.9rem 2rem;
    border: 1px solid var(--border-light);
    border-radius: 100px;
    background: rgba(10, 10, 10, 0.7);
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.2em;
    color: var(--text-secondary);
}

.hero-strip span:nth-child(odd) {
    color: var(--accent);
}

/* --- Content section below the hero --- */
.after-hero {
    max-width: 620px;
    padding: 2rem 0 4rem;
}

.after-hero-title {
    font-size: clamp(1.75rem, 4vw, 2.5rem);
    margin: 0.75rem 0 1.5rem;
}

.after-hero-copy {
    color: var(--text-secondary);
    margin-bottom: 1.25rem;
}

/* ============================================
   RESPONSIVE
   ============================================ */
@media (max-width: 768px) {
    .container {
        padding: 3rem 1.5rem;
    }

    .page-header {
        margin-bottom: 4rem;
    }

    .example {
        margin-bottom: 5rem;
        padding-bottom: 3rem;
    }

    .example-content {
        padding-left: 0;
    }

    .parallax-hero {
        height: 80vh;
        min-height: 460px;
        border-radius: 16px;
    }

    .hero-strip {
        gap: 0.5rem 1.25rem;
        padding: 0.75rem 1.25rem;
    }
}

/* ============================================
   REDUCED MOTION
   ============================================ */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }

    .parallax-layer {
        transform: none !important;
        opacity: 1 !important;
        will-change: auto;
    }
}

/* ============================================
   SCREEN READER ONLY (for ARIA live regions)
   ============================================ */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

A layered hero section where elements move at different speeds while scrolling, creating depth with a single scrubbed ScrollTrigger per container.

Quick Start

1. Add to your HTML <head>:

<link rel="stylesheet" href="path/to/style.css">

2. Add before closing </body> tag:

<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/ScrollTrigger.min.js"></script>
<script src="path/to/script.js"></script>

3. Add the effect HTML anywhere in your <body>:

<section class="parallax-hero" data-parallax>
  <div class="parallax-layer" data-parallax-speed="0.2">
    <!-- Slow background: gradient shapes, textures -->
  </div>
  <div class="parallax-layer" data-parallax-speed="0.7" data-parallax-fade>
    <h1>Your Headline</h1>
  </div>
  <div class="parallax-layer" data-parallax-speed="1.3">
    <!-- Fast foreground: badges, labels -->
  </div>
</section>

That is the whole setup. The script finds every [data-parallax] container, collects its [data-parallax-speed] layers, and wires up one scrubbed ScrollTrigger per container.

Options

Attribute Values Default Description
data-parallax (none, marker) required Marks a container as a parallax scene. One ScrollTrigger is created per container
data-parallax-speed Any number 1 Layer speed relative to the scroll. 1 tracks the scroll exactly, values below 1 lag behind (background), values above 1 race ahead (foreground)
data-parallax-fade (none, marker) off Fades the layer out (opacity and visibility) as the container leaves the top of the viewport

Choosing speed values

The layer's vertical shift is proportional to (1 - speed), so the further a value sits from 1, the more the layer separates from the page:

  • 0.2 deep background, barely moves
  • 0.5 mid background, clearly lags
  • 0.9 near-normal, subtle drift
  • 1 moves with the page (no parallax)
  • 1.3 foreground, moves faster than the scroll

Examples

Minimal Two-Layer Hero

HTML:

<section class="parallax-hero" data-parallax>
  <div class="parallax-layer" data-parallax-speed="0.3">
    <div class="hero-blob hero-blob--violet"></div>
  </div>
  <div class="parallax-layer" data-parallax-speed="0.8">
    <h1>Depth Without Images</h1>
  </div>
</section>

Headline That Fades On Exit

HTML:

<div class="parallax-layer" data-parallax-speed="0.7" data-parallax-fade>
  <h1>Fades as the hero leaves the viewport</h1>
</div>

Multiple Scenes On One Page

Each data-parallax container is independent, so you can repeat the pattern for section headers further down the page:

HTML:

<section class="parallax-hero" data-parallax>...</section>

<section class="parallax-hero" data-parallax>
  <div class="parallax-layer" data-parallax-speed="0.4">
    <div class="hero-grid"></div>
  </div>
  <div class="parallax-layer" data-parallax-speed="1.1">
    <h2>Second Scene</h2>
  </div>
</section>

CSS Classes

Class Description
.parallax-hero The scene container: sets height, overflow: clip, and background
.parallax-layer Absolutely positioned layer that fills the hero, with will-change: transform
.hero-blob Pure CSS radial-gradient shape for background depth
.hero-grid Faint dot grid layer, masked to the center of the hero
.hero-strip Foreground label strip (JetBrains Mono, pill-shaped)

Layers use inset: -12% 0 so they overscan the container vertically; this hides the edges that would otherwise be revealed as layers shift. If you use aggressive speeds (below 0.2 or above 1.5), increase the overscan to match.

How It Works

Each container gets one GSAP timeline with scrub: true, running from clamp(top bottom) to clamp(bottom top), so timeline progress maps to the container's full journey through the viewport. Every layer receives a fromTo tween on yPercent between -(1 - speed) * 50 and (1 - speed) * 50. The clamp() wrapper prevents a visual jump when the hero is already on screen at load, and invalidateOnRefresh recalculates on resize.

Accessibility

  • Reduced motion: respects prefers-reduced-motion. The gsap.matchMedia reduce branch pins every layer at yPercent: 0 with full opacity, and a CSS rule enforces transform: none on layers, so all content stays visible and static
  • No interaction required: the effect is purely scroll-driven, so there are no hover or keyboard traps
  • Decorative layers: mark purely visual layers (blobs, grids) with aria-hidden="true" so screen readers skip them, as the demo does
  • Content order: keep the headline layer as real heading markup (<h1>/<h2>) so the document outline survives the visual layering

Performance Notes

  • Only yPercent (a transform) and autoAlpha are animated, so all work stays on the compositor
  • will-change: transform and force3D: true promote layers to their own GPU layers
  • One ScrollTrigger per container, regardless of layer count

Browser Support

Modern browsers (ES6+). Not compatible with IE11.

Dependencies

Required:

  • GSAP 3.12+
  • ScrollTrigger plugin

Optional:

  • Lenis (smooth scroll integration; the script auto-detects it and the effect works without it)

Your Cart

Your cart is empty

Browse Effects