Most digital experiences finish once you cease scrolling.
This one rewards you for not stopping.
On this tutorial, we’re breaking down a small however mighty scroll interplay constructed round infinite looping, parallax depth, and snap-based management. Not from a “have a look at this cool impact” angle, however from the choices that make it really feel easy, intentional, and unusually addictive.
The unique model was created for a consumer undertaking for Jillian Phyllis. The temporary was easy: create one thing light-weight, quick, and MVP-friendly.
Which, as everyone knows, is often the place the hazard begins.
Easy can simply turn out to be empty. Minimal can simply turn out to be forgettable. So the purpose wasn’t so as to add extra, it was to make much less really feel like extra.
The reply was movement.
A seamless loop removes the onerous cease on the backside of the web page. Parallax introduces depth between sections. Snap management provides the expertise rhythm, so the consumer isn’t simply flinging via content material like a buying trolley with one damaged wheel.
Below the hood, the whole lot is powered by Lenis and GSAP. The unique undertaking used Subsequent.js, TypeScript, and Styled Elements, however for this tutorial we’re stripping it again to plain HTML, CSS, and JavaScript.
No framework dependency.
No pointless ceremony.
Simply the interplay, the structure, and the logic behind it.
For this construct, three issues matter:
- True infinite looping with out seen seams
- Parallax movement that provides depth, not ornament
- Snap-based scroll management that feels deliberate
From this level on, we’ll stroll via the implementation step-by-step.
Clear code. Small snippets. No magic tips.
Effectively, perhaps one or two.
Structure Overview
Earlier than writing any animation code, it helps to know the form of the system.
We’re making a steady scroll expertise, however the trick is that the web page itself ought to by no means really feel prefer it resets. The loop must occur invisibly.
The core concept is easy:
- Create fullscreen sections
- Duplicate the primary part on the finish
- Allow infinite scrolling in Lenis
- Snap to every part
- Animate the media inside every part with GSAP ScrollTrigger
That provides us the muse: the consumer sees a easy, steady web page.
The browser sees a fastidiously staged loop sporting an excellent disguise.
Preliminary Setup
After I first constructed this for LinkedIn, I used my traditional stack: Subsequent.js, TypeScript, and Styled Elements.
For this tutorial, we’re doing the exact opposite.
For this tutorial, we’re stripping it again to plain HTML, CSS, and JavaScript and powering it with Lenis and GSAP.
Your instruments, your guidelines. I’ll information the construction.
HTML
We begin with a couple of fullscreen sections. Three is the candy spot:
- 2 feels empty
- 3+ feels intentional
Every part comprises media. Photos, video, canvas, WebGL, take your decide.
The vital half is that this:
We duplicate the primary part and place it on the finish.
That is what permits Lenis to loop seamlessly. With out it, you’ll really feel the reset. With it, the loop disappears.
We additionally conceal this duplicate from assistive tech utilizing aria-hidden so display readers don’t learn it twice.
<head>
<!-- CSS Reset -->
<hyperlink rel="stylesheet" href="./property/reset.css">
<!-- Lenis -->
<hyperlink rel="stylesheet" href="https://unpkg.com/lenis@1.3.23/dist/lenis.css">
<!-- Customized Kinds -->
<hyperlink rel="stylesheet" href="./kinds.css">
</head>
<physique>
<part class="hero">
<image class="hero-image">
<img src="..." alt="Picture 1" />
</image>
</part>
<part class="hero">
<image class="hero-image">
<img src="..." alt="Picture 2" />
</image>
</part>
<part class="hero">
<image class="hero-image">
<img src="..." alt="Picture 3" />
</image>
</part>
<!-- Duplicate -->
<part class="hero" aria-hidden="true">
<image class="hero-image" aria-hidden="true">
<img src="..." alt="" aria-hidden="true" />
</image>
</part>
<!-- 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>
</physique>
CSS
Every part fills the viewport.
We use 100svh as a substitute of 100vh to keep away from cellular Safari toolbar points.
Construction-wise:
Part: Fullscreen → Media Container: Fill Container → Media: Fill Container.
.hero {
place: relative;
overflow: clip;
show: grid;
place-items: middle;
width: 100%;
top: 100svh;
& image, & img {
show: block;
}
& image {
place: absolute;
inset: 0;
z-index: -1;
& img {
width: 100%;
top: 100%;
object-fit: cowl;
}
}
}
JavaScript
We cut up the logic into two elements:
- Scroll system
- Parallax animation
Lenis Setup
That is the place the loop occurs.
gsap.registerPlugin(ScrollTrigger);
const setupLenis = () => {
const lenis = new Lenis({
infinite: true,
});
const snap = new Snap(lenis, {
sort: 'necessary',
debounce: 500,
period: 0.9,
easing: (t) => 1 - Math.pow(1 - t, 4),
});
const sections = doc.querySelectorAll('part');
snap.addElements(sections, {
align: 'begin',
});
lenis.on('scroll', ScrollTrigger.replace);
gsap.ticker.add((time) => {
lenis.raf(time * 1000);
});
gsap.ticker.lagSmoothing(0);
};
One line does many of the magic:
infinite: true
Every thing else is management and polish.
Parallax
That is the place it goes from “works” to “feels good”.
const sectionAnimation = () => {
const heros = doc.querySelectorAll('.hero');
heros.forEach((hero) => {
const media = hero.querySelector('image');
gsap.set(media, { yPercent: -50 });
gsap.fromTo(
media,
{ yPercent: -50 },
{
yPercent: 50,
ease: 'none',
scrollTrigger: {
set off: hero,
begin: 'high backside',
finish: 'backside high',
scrub: true,
},
}
);
});
};
We transfer from -50% to 50%.
That’s it.
That slight delay between scroll and media creates depth. Layers begin interacting. The web page stops feeling flat.
You possibly can view a minimal replica of this over on Codepen right here:
Right here’s a video of the impact:
Superior Integrations
As soon as the core is working, you can begin including finesse.
For this undertaking, I layered in a curved marquee utilizing an OSMO element (Curved Marquee). I closely customised it to suit my particular wants, however didn’t reinvent the wheel.
Typically the neatest transfer is understanding when to not write extra code.
Two marquees, completely different speeds, delicate scaling utilizing ScrollTrigger.
Small element. Large payoff.
Last Polish (iOS Repair)
Every thing works… till you open Safari on iOS.
Then the toolbar ruins your day.
Because it expands and collapses, it exposes the loop seam. The phantasm breaks.
So we repair it correctly.
Nested Lenis Setup
We outline our personal scroll container:
HTML
<div class="wrapper">
<div class="content material">
<!-- sections -->
</div>
</div>
CSS
.wrapper {
place: relative;
top: 100svh;
overflow: hidden;
}
JS Integration
We inform Lenis and GSAP to make use of this container:
const wrapper = doc.querySelector('.wrapper');
const content material = doc.querySelector('.content material');
const lenis = new Lenis({
infinite: true,
wrapper: wrapper,
content material: content material,
});
ScrollTrigger.scrollerProxy(wrapper, {
scrollTop(worth) {
if (arguments.size) {
lenis.scrollTo(worth, { quick: true });
} else {
return lenis.scroll;
}
},
getBoundingClientRect() {
return {
high: 0,
left: 0,
width: wrapper.clientWidth,
top: wrapper.clientHeight,
};
},
pinType: 'remodel',
});
And replace the ScrollTrigger animations:
scroller: wrapper
Now the loop is definitely seamless.
No flicker.
No bounce.
No Safari chaos.
Wrap-up
And that’s the total system: a seamless infinite scroll layered with parallax depth that shifts the expertise from one thing you merely navigate to one thing you really really feel. There’s no heavy structure or over-engineering behind it, only a handful of well-considered concepts working collectively in the appropriate manner.
That’s the true takeaway right here. It’s not often about including extra, it’s about understanding what issues and executing it with intent. Take this, adapt it, push it additional, and construct one thing individuals genuinely wish to maintain scrolling.


