惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Check Point Blog
U
Unit 42
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Martin Fowler
Martin Fowler
L
LangChain Blog
博客园_首页
博客园 - 【当耐特】
Vercel News
Vercel News
I
InfoQ
GbyAI
GbyAI
爱范儿
爱范儿
D
DataBreaches.Net
Blog — PlanetScale
Blog — PlanetScale
B
Blog RSS Feed
A
About on SuperTechFans
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
G
Google Developers Blog
大猫的无限游戏
大猫的无限游戏
Apple Machine Learning Research
Apple Machine Learning Research
F
Fortinet All Blogs
N
Netflix TechBlog - Medium
酷 壳 – CoolShell
酷 壳 – CoolShell
P
Proofpoint News Feed
美团技术团队
V
V2EX
Stack Overflow Blog
Stack Overflow Blog
有赞技术团队
有赞技术团队
Y
Y Combinator Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
H
Help Net Security
Recent Announcements
Recent Announcements
Microsoft Azure Blog
Microsoft Azure Blog
D
Docker
宝玉的分享
宝玉的分享
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
小众软件
小众软件
J
Java Code Geeks
S
SegmentFault 最新的问题
Engineering at Meta
Engineering at Meta
Google DeepMind News
Google DeepMind News
MongoDB | Blog
MongoDB | Blog
The Cloudflare Blog
Recorded Future
Recorded Future
阮一峰的网络日志
阮一峰的网络日志
T
The Blog of Author Tim Ferriss
MyScale Blog
MyScale Blog
Microsoft Security Blog
Microsoft Security Blog

Codrops

ZERO: The Engineering Behind a Defiant Interactive Narrative | Codrops Meet the Speakers of the First Three.js Conference | Codrops The Architecture Behind Trionn: Coordinating GSAP, Three.js, Lenis, and Web Audio | Codrops Where Stories Become Interactive Worlds: How makemepulse Brings Ideas to Life Across Web, XR, and Beyond | Codrops The Sleepers: Creating an Atmospheric WebGL Experience with Lightweight Techniques Building an Interactive Wave Propagation Cube Grid with Three.js | Codrops Ten Years Away: Designing an Interactive Comic for Studio375’s Tenth Anniversary | Codrops Building a Scroll-Driven 3D Gallery Using a Blender Camera Path with Three.js and GSAP | Codrops From Years of Client Work to a Next.js + Sanity Starter Kit | Codrops Sculpting a Digital Athlete: Capturing Stefanos Tsitsipas Beyond the Court | Codrops Building Persistent Page Transitions with WebGPU and Vanilla JavaScript | Codrops Inside Bisous: Designing an Editorial Experience for Cinematic CGI | Codrops Engineering the Web Experience Behind Shopify’s Spring ’26 Edition: Everywhere | Codrops Shaping Stories into Experience: The Work of Kevin Lam | Codrops Testing What Users Actually See with Vitest and Chromatic | Codrops Podium: Building a Website Where Running Becomes Storytelling | Codrops Designing Beyond the Surface: How DashDigital Turns Complexity into Clarity | Codrops Creative Entrepreneurship: Designing the Machine | Codrops Exploring 3D Image Rotations on Scroll | Codrops Always Building, Always Learning: Cyd Stumpel’s Journey Through the Modern Web | Codrops Designing A Better Lou: Reducing Cognitive Load Through Design, Content, and Systems | Codrops Sketching the Impossible: A 3D Portfolio Built Without a Single 3D Model | Codrops Building Horeca: Advanced Motion Design in Webflow Without the Performance Trade-Offs | Codrops Building an Interactive Digital Stamp Collection with Shaders, Postcards, and Playful Inspection | Codrops No Two Paths Alike: Inside San Rita’s Approach to Digital Experiences | Codrops The Making of the New Lesse Studio Website: Clarity, Performance, and Intentionality | Codrops Creating a Thumbnail Flow Animation with GSAP MotionPath | Codrops Forging Her Own Path: Houmahani Kane’s Journey in Creative Development | Codrops Beaucoup and the Pursuit of Memorable Experiences | Codrops The Future of Emotional Technology: Comfort Websites for Growth and Self-Awareness | Codrops Whooshes, Snaps and Shaders: Adrien Vanderpotte and the Feeling of the Interface | Codrops Building a Scroll-Driven 3D Cube Gallery in Webflow with GSAP | Codrops Creating Scroll-Driven SVG Map Animations with GSAP | Codrops Made With Gsap: Building a Fun Gravity-Based Mouse Trail | Codrops 80s Business Tech and Seamless Scene Transitions: Inside Shader.se’s Scroll-Driven WebGPU Pipeline | Codrops Merlin: The Code Boutique Turning Motion Into Digital Magic | Codrops Designing Ourselves: The New Obys Identity and Website | Codrops Exploring the HTML-in-Canvas Proposal | Codrops Le:mma Studio: Building the Feeling Behind the Screen | Codrops From Shader Uniforms to Clip-Path Wipes: How GSAP Drives My Portfolio | Codrops Reverse-Engineering Claude AI’s Mascot Animations with SVG and GSAP | Codrops Akaru: Crafting Precision-Driven Digital Experiences | Codrops Designing Against the Gallery: A Two-Year Journey to a Layered Portfolio Experience | Codrops Between Human and Machine: Sofia Papadopoulou’s Approach to Augmented Creativity | Codrops More Than a Portfolio: Building a Scroll-Driven 3D World with Something to Say | Codrops Rhumb Studio: Small but Mighty, Shaped by Curiosity | Codrops Susurrus: Crafting a Cozy Watercolor World with Three.js and Shaders | Codrops Building 4WIDE: Turning Distortion, Blur, and Motion into a Coherent Experience | Codrops A Playful Clip Menu with GSAP’s easeReverse | Codrops False Earth: From WebGL Limits to a WebGPU-Driven World | Codrops Interactive Storytelling for the Web: Building Immersive Stories with Timelines, 3D, and Layered Scenes | Codrops The Art of Complex Motion: Corentin Bernadou’s World of Shaders and Experimentation | Codrops They Call Me Giulio: The Making of a Cinematic Cyberpunk Portfolio | Codrops Lusion: Where Digital Craft Meets Ambitious Experimentation | Codrops The Exat Microsite: Pushing a Typography Showcase to New Creative Extremes | Codrops A Single DM, a First SOTD, and the Start of Chanh M. Ho’s Creative Journey | Codrops Creating Custom Page Transitions in Astro with Barba.js and GSAP | Codrops R—K ’26: The Thinking and Code Behind a Portfolio Led by Presence | Codrops Building the Maxima Therapy Website: React, GSAP, and Dabbling with AI | Codrops Where Engineering Meets Craft: Edoardo Lunardi’s Obsession with the Details | Codrops Animating 160,000 Cubes in Three.js to Visualize Dithering | Codrops Arnaud Rocca’s Portfolio: From a GSAP-Powered Motion System to Fluid WebGL | Codrops Where Confidence Needs No Decoration: The Malvah.Studio Story | Codrops Motion Highlights #17 | Codrops
The Never Ending Story: Building a Seamless Infinite Scroll Experience with GSAP & Lenis | Codrops
By Joe Ben Taylor · 2026-05-28 · via Codrops

Most digital experiences end when you stop scrolling.
This one rewards you for not stopping.

In this tutorial, we’re breaking down a small but mighty scroll interaction built around infinite looping, parallax depth, and snap-based control. Not from a “look at this cool effect” angle, but from the decisions that make it feel smooth, intentional, and strangely addictive.

The original version was created for a client project for Jillian Phyllis. The brief was simple: create something lightweight, fast, and MVP-friendly.

Which, as we all know, is usually where the danger starts.

Simple can easily become empty. Minimal can easily become forgettable. So the goal wasn’t to add more, it was to make less feel like more.

The answer was motion.

A seamless loop removes the hard stop at the bottom of the page. Parallax introduces depth between sections. Snap control gives the experience rhythm, so the user isn’t just flinging through content like a shopping trolley with one broken wheel.

Under the hood, everything is powered by Lenis and GSAP. The original project used Next.js, TypeScript, and Styled Components, but for this tutorial we’re stripping it back to plain HTML, CSS, and JavaScript.

No framework dependency.
No unnecessary ceremony.
Just the interaction, the architecture, and the logic behind it.

For this build, three things matter:

  1. True infinite looping without visible seams
  2. Parallax motion that adds depth, not decoration
  3. Snap-based scroll control that feels deliberate

From this point on, we’ll walk through the implementation step by step.

Clear code. Small snippets. No magic tricks.

Well, maybe one or two.

Architecture Overview

Before writing any animation code, it helps to understand the shape of the system.

We are creating a continuous scroll experience, but the trick is that the page itself should never feel like it resets. The loop needs to happen invisibly.

The core idea is simple:

  • Create fullscreen sections
  • Duplicate the first section at the end
  • Enable infinite scrolling in Lenis
  • Snap to each section
  • Animate the media inside each section with GSAP ScrollTrigger

That gives us the foundation: the user sees a smooth, continuous page.

The browser sees a carefully staged loop wearing a very good disguise.

Initial Setup

When I first built this for LinkedIn, I used my usual stack: Next.js, TypeScript, and Styled Components.

For this tutorial, we’re doing the complete opposite.

For this tutorial, we’re stripping it back to plain HTML, CSS, and JavaScript and powering it with Lenis and GSAP.

Your tools, your rules. I’ll guide the structure.

HTML

We start with a few fullscreen sections. Three is the sweet spot:

  • 2 feels empty
  • 3+ feels intentional

Each section contains media. Images, video, canvas, WebGL, take your pick.

The important part is this:

We duplicate the first section and place it at the end.

This is what allows Lenis to loop seamlessly. Without it, you’ll feel the reset. With it, the loop disappears.

We also hide this duplicate from assistive tech using aria-hidden so screen readers don’t read it twice.

<head>
    <!-- CSS Reset -->
    <link rel="stylesheet" href="./assets/reset.css">

    <!-- Lenis -->
    <link rel="stylesheet" href="https://unpkg.com/lenis@1.3.23/dist/lenis.css">

    <!-- Custom Styles -->
    <link rel="stylesheet" href="./styles.css">
</head>

<body>
    <section class="hero">
        <picture class="hero-image">
            <img src="..." alt="Image 1" />
        </picture>
    </section>

    <section class="hero">
        <picture class="hero-image">
            <img src="..." alt="Image 2" />
        </picture>
    </section>

    <section class="hero">
        <picture class="hero-image">
            <img src="..." alt="Image 3" />
        </picture>
    </section>

    <!-- Duplicate -->
    <section class="hero" aria-hidden="true">
        <picture class="hero-image" aria-hidden="true">
            <img src="..." alt="" aria-hidden="true" />
        </picture>
    </section>

    <!-- Scripts -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
    <script src="https://unpkg.com/lenis@1.3.23/dist/lenis.min.js"></script>
    <script src="https://unpkg.com/lenis@1.3.23/dist/lenis-snap.min.js"></script>
    <script src="./scripts.js"></script>
</body>

CSS

Each section fills the viewport.

We use 100svh instead of 100vh to avoid mobile Safari toolbar issues.

Structure-wise:

Section: Fullscreen → Media Container: Fill Container → Media: Fill Container.

.hero {
    position: relative;
    overflow: clip;

    display: grid;
    place-items: center;

    width: 100%;
    height: 100svh;

    & picture, & img {
        display: block;
    }

    & picture {
        position: absolute;
        inset: 0;
        z-index: -1;

        & img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }
    }
}

JavaScript

We split the logic into two parts:

  1. Scroll system
  2. Parallax animation

Lenis Setup

This is where the loop happens.

gsap.registerPlugin(ScrollTrigger);

const setupLenis = () => {
    const lenis = new Lenis({
        infinite: true,
    });

    const snap = new Snap(lenis, {
        type: 'mandatory',
        debounce: 500,
        duration: 0.9,
        easing: (t) => 1 - Math.pow(1 - t, 4),
    });

    const sections = document.querySelectorAll('section');

    snap.addElements(sections, {
        align: 'start',
    });

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

    gsap.ticker.add((time) => {
        lenis.raf(time * 1000);
    });

    gsap.ticker.lagSmoothing(0);
};

One line does most of the magic:

infinite: true

Everything else is control and polish.

Parallax

This is where it goes from “works” to “feels good”.

const sectionAnimation = () => {
    const heros = document.querySelectorAll('.hero');

    heros.forEach((hero) => {
        const media = hero.querySelector('picture');

        gsap.set(media, { yPercent: -50 });

        gsap.fromTo(
            media,
            { yPercent: -50 },
            {
                yPercent: 50,
                ease: 'none',
                scrollTrigger: {
                    trigger: hero,
                    start: 'top bottom',
                    end: 'bottom top',
                    scrub: true,
                },
            }
        );
    });
};

We move from -50% to 50%.

That’s it.

That slight delay between scroll and media creates depth. Layers start interacting. The page stops feeling flat.

You can view a minimal reproduction of this over on Codepen here:

Here’s a video of the effect:

Advanced Integrations

Once the core is working, you can start adding finesse.

For this project, I layered in a curved marquee using an OSMO component (Curved Marquee). I heavily customised it to fit my specific needs, but didn’t reinvent the wheel.

Sometimes the smartest move is knowing when not to write more code.

Two marquees, different speeds, subtle scaling using ScrollTrigger.

Small detail. Big payoff.

Final Polish (iOS Fix)

Everything works… until you open Safari on iOS.

Then the toolbar ruins your day.

As it expands and collapses, it exposes the loop seam. The illusion breaks.

So we fix it properly.

Nested Lenis Setup

We define our own scroll container:

HTML

<div class="wrapper">
    <div class="content">
        <!-- sections -->
    </div>
</div>

CSS

.wrapper {
    position: relative;
    height: 100svh;
    overflow: hidden;
}

JS Integration

We tell Lenis and GSAP to use this container:

const wrapper = document.querySelector('.wrapper');
const content = document.querySelector('.content');

const lenis = new Lenis({
    infinite: true,
    wrapper: wrapper,
    content: content,
});

ScrollTrigger.scrollerProxy(wrapper, {
    scrollTop(value) {
        if (arguments.length) {
            lenis.scrollTo(value, { immediate: true });
        } else {
            return lenis.scroll;
        }
    },
    getBoundingClientRect() {
        return {
            top: 0,
            left: 0,
            width: wrapper.clientWidth,
            height: wrapper.clientHeight,
        };
    },
    pinType: 'transform',
});

And update the ScrollTrigger animations:

scroller: wrapper

Now the loop is actually seamless.

No flicker.
No jump.
No Safari chaos.

Wrap-up

And that’s the full system: a seamless infinite scroll layered with parallax depth that shifts the experience from something you simply navigate to something you actually feel. There’s no heavy architecture or over-engineering behind it, just a handful of well-considered ideas working together in the right way.

That’s the real takeaway here. It’s rarely about adding more, it’s about understanding what matters and executing it with intent. Take this, adapt it, push it further, and build something people genuinely want to keep scrolling.