Getting Creative with Infinite Loop Scrolling

A quick look at how to recreate the infinite loop scrolling animation seen on Bureau DAM with GSAP and Lenis.

Infinite scrolling is a web design technique that allows users to scroll through a never-ending list of content by automatically loading new items as the user reaches the bottom of the page. Instead of having to click through to a new page to see more content, the content is automatically loaded and appended to the bottom of the page as the user scrolls. This can create a seamless and engaging browsing experience for users, as they can easily access a large amount of content without having to wait for new pages to load. Forget about reaching the footer, though!

Looping the scroll of a page refers to the process of automatically taking users back to the top of the page once they reach the end of the scroll. This means that they will be able to continuously scroll through the same content over and over again, rather than being presented with new content as they scroll down the page.

In this article, we will show some examples creative loop scrolling, and then reimplement the effect seem on Bureau DAM. We will be using Lenis by Studio Freight to implement the looping effect and GSAP for the animations.

What is loop scrolling?

When your content is limited, you can do creative things with loop scrolling, which is basically infinite scrolling with repeating content. A great example for such a creative use is the effect seen on Bureau DAM:

Bureau DAM employs a fun looping scroll effect

Some great ideas how looping the scroll can be used in a creative way and add value to a website:

  • It can create an immersive experience for users. By continuously scrolling through the same content, users can feel more immersed in the website and engaged with the content. This can be particularly useful for websites that want to create a strong emotional connection with their audience.
  • It can be used to create a game or interactive experience. By adding interactive elements to the content that is being looped, developers can create a game or interactive experience for users. For example, a website could use looping the scroll to create a scrolling game or to allow users to interact with the content in a novel way.
  • It can be used to create a “time loop” effect. By looping the scroll in a particular way, developers can create the illusion of a “time loop,” where the content appears to repeat itself in a continuous loop. This can be particularly effective for websites that want to convey a sense of continuity or timelessness.

A simple example

Let’s create a simple example. We’ll set up a grid of 6 images that repeat on scroll. How can we achieve this? It’s simple: we need to repeat the content in a way that when reaching the end, we can simply reset the scroll to the top without anybody noticing! We’ve previously explored this concept in our CSS-only marquee effect. In another demo we also show how to play with this kind of infinite animation.

So, for our first example we have the following markup:

<div class="grid">
	<div class="grid__item" style="background-image:url(img/1.jpg);"></div>
	<div class="grid__item" style="background-image:url(img/4.jpg);"></div>
	<div class="grid__item" style="background-image:url(img/3.jpg);"></div>
	<div class="grid__item" style="background-image:url(img/5.jpg);"></div>
	<div class="grid__item" style="background-image:url(img/2.jpg);"></div>
	<div class="grid__item" style="background-image:url(img/6.jpg);"></div>
</div>

Our styles will create a 3×3 grid:

.grid {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: 5vh;
}

.grid__item {
	height: 47.5vh; /* 50vh minus half of the gap */
	background-size: cover;
	background-position: 50% 20%;
}

.grid__item:nth-child(3n-2) {
	border-radius: 0 2rem 2rem 0;
}

.grid__item:nth-child(3n) {
	border-radius: 2rem 0 0 2rem;
}

.grid__item:nth-child(3n-1) {
	border-radius: 2rem;
}

Lenis comes with a handy option for making the scroll infinite. In our script we’ll make sure to repeat the visible grid items (in this case it’s 6):

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

function raf(time) {
    lenis.raf(time);
    requestAnimationFrame(raf);
}

requestAnimationFrame(raf);

// repeat first six items by cloning them and appending them to the .grid
const repeatItems = (parentEl, total = 0) => {
    const items = [...parentEl.children];
    for (let i = 0; i <= total-1; ++i) {
        var cln = items[i].cloneNode(true);
        parentEl.appendChild(cln);
    }
};
repeatItems(document.querySelector('.grid'), 6);

The result is a grid that repeats on scroll:

A simple example of loop scrolling. View the demo

We can also change the direction of the scroll and go sideways!

View the demo

Playing with animations

While we scroll, we can add some fancy animations to our grid items. Paired with switching the transform origin in the right time, we can get something playful like this:

Scale animation while scrolling. View the demo

We can also play with the scale and opacity values to create something like a vanishing effect:

View the demo

Or, add some extreme stretchiness:

View the demo

We can also play with a 3D animation and an additional filter effect:

Adding a filter effect and perspective. View the demo

The Bureau DAM example

Now, let’s see how we can remake the Bureau DAM animation. As we don’t have a grid here, things get a bit simpler. Just like them, we’ll use an SVG for the typography element, as we want to stretch it over the screen:

<div class="grid">
	<div class="grid__item grid__item--stack">
		<svg class="grid__item-logo" width="100%" height="100%" viewBox="0 0 503 277" preserveAspectRatio="none">
		<path d="M56.3 232.3 56.3 193.8C56.3 177.4 54.7 174.1 48.5 165.9 35.4 148.8 17.6 133 8.5 120.8.7 110.3.1 103.7.1 85.6L.1 45.2C.1 14.9 13.5.5 41 .5 68.8.5 79.1 15.3 79.1 45.2L79.1 94.5 56.9 94.5 56.9 48.5C56.9 35 53.5 25.8 40.7 25.8 29.8 25.8 24.1 32.4 24.1 45.2L24.1 85.3C24.1 96.8 25.1 100.1 29.8 106.3 41 121.8 59.1 137.6 68.8 150.4 77.2 161.6 80 169.8 80 193.5L80 232.3C80 260.9 68.8 277 40.4 277 12.3 277 .1 261.5.1 232.3L.1 174.7 22.9 174.7 22.9 228.7C22.9 243.1 26.9 252.3 40.1 252.3 51.6 252.3 56.3 245.1 56.3 232.3ZM176.5 277 101.5 277 101.5.5 127.1.5 127.1 251.8 176.5 251.8 176.5 277ZM290 277 264.5 277 258.4 230.6 217.1 230.6 211 277 186.2 277 224.1.5 254.1.5 290 277ZM218.1 207.1 253.4 207.1C247.7 159.7 241.6 114 236.3 65.3 230.5 114 224.5 159.7 218.1 207.1ZM399.6 277 374 277 326.3 75.1C326.6 117.1 326.6 155.7 326.6 197.7L326.6 277 304.5 277 304.5.5 335 .5 377.4 203.1C377 165.1 377 129.2 377 91.2L377 .5 399.6.5 399.6 277ZM471.5 277 446.3 277 446.3 26.3 415.3 26.3 415.3.5 502.4.5 502.4 26.3 471.5 26.3 471.5 277Z" id="SLANT" fill="#fff"></path>
		</svg>
		<p class="grid__item-text credits">An infinite scrolling demo based on <a href="https://www.bureaudam.com/">Bureau DAM</a></p>
	</div>
	<div class="grid__item">
		<div class="grid__item-inner">
			<div class="grid__item-img" style="background-image:url(img/1.jpg);"></div>
			<p class="grid__item-text"><a href="#">View all projects →</a></p>
		</div>
	</div>
</div>

In our CSS we set a couple of styles that will make sure the items are stretched to full screen:

.grid {
    display: flex;
    flex-direction: column;
    gap: 5vh;
}

.grid__item {
    height: 100vh; 
	place-items: center;
    display: grid;
}

.grid__item-inner {
	display: grid;
	gap: 1rem;
	place-items: center;
	text-align: center;
}

.grid__item--stack {
	display: grid;
	gap: 2rem;
	grid-template-rows: 1fr auto;
}

.grid__item-logo {
	padding: 8rem 1rem 0;
}

.grid__item-img {
	background-size: cover;
    background-position: 50% 50%;
	height: 70vh;
	aspect-ratio: 1.5;
}

.grid__item-text {
	margin: 0;
}

A proper setting for the transform origins ensures that we get the right visual effect:

gsap.registerPlugin(ScrollTrigger);

// repeat first three items by cloning them and appending them to the .grid
const repeatItems = (parentEl, total = 0) => {
    const items = [...parentEl.children];
    for (let i = 0; i <= total-1; ++i) {
        var cln = items[i].cloneNode(true);
        parentEl.appendChild(cln);
    }
};

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

lenis.on('scroll',()=>{
    ScrollTrigger.update() // Thank you Clément!
})

function raf(time) {
    lenis.raf(time);
    requestAnimationFrame(raf);
}

imagesLoaded( document.querySelectorAll('.grid__item'), { background: true }, () => {

    document.body.classList.remove('loading');

    repeatItems(document.querySelector('.grid'), 1);

    const items = [...document.querySelectorAll('.grid__item')];

    // first item
    const firtsItem = items[0];
    gsap.set(firtsItem, {transformOrigin: '50% 100%'})
    gsap.to(firtsItem, {
        ease: 'none',
        startAt: {scaleY: 1},
        scaleY: 0,
        scrollTrigger: {
            trigger: firtsItem,
            start: 'center center',
            end: 'bottom top',
            scrub: true,
            fastScrollEnd: true,
            onLeave: () => {
                gsap.set(firtsItem, {scaleY: 1,})
            },
        }
    });

    // last item  
    const lastItem = items[2];
    gsap.set(lastItem, {transformOrigin: '50% 0%', scaleY: 0})
    gsap.to(lastItem, {
        ease: 'none',
        startAt: {scaleY: 0},
        scaleY: 1,
        scrollTrigger: {
            trigger: lastItem,
            start: 'top bottom',
            end: 'bottom top',
            scrub: true,
            fastScrollEnd: true,
            onLeaveBack: () => {
                gsap.set(lastItem, {scaleY: 1})
            }
        }
    });
    
    // in between
    let ft;
    let st;
    const middleItem = items[1];
        
    ft = gsap.timeline()
    .to(middleItem, {
        ease: 'none',
        onStart: () => {
            if (st) st.kill()
        },
        startAt: {scale: 0},
        scale: 1,
        scrollTrigger: {
            trigger: middleItem,
            start: 'top bottom',
            end: 'center center',
            scrub: true,
            onEnter: () => gsap.set(middleItem, {transformOrigin: '50% 0%'}),
            onEnterBack: () => gsap.set(middleItem, {transformOrigin: '50% 0%'}),
            onLeave: () => gsap.set(middleItem, {transformOrigin: '50% 100%'}),
            onLeaveBack: () => gsap.set(middleItem, {transformOrigin: '50% 100%'}),
        },
    });

    st = gsap.timeline()
    .to(middleItem, {
        ease: 'none',
        onStart: () => {
            if (ft) ft.kill()
        },
        startAt: {scale: 1},
        scale: 0,
        scrollTrigger: {
            trigger: middleItem,
            start: 'center center',
            end: 'bottom top',
            scrub: true,
            onEnter: () => gsap.set(middleItem, {transformOrigin: '50% 100%'}),
            onEnterBack: () => gsap.set(middleItem, {transformOrigin: '50% 100%'}),
            onLeave: () => gsap.set(middleItem, {transformOrigin: '50% 0%'}),
            onLeaveBack: () => gsap.set(middleItem, {transformOrigin: '50% 0%'}),
        },
    });
    
    requestAnimationFrame(raf);
    
    const refresh = () => {
        ScrollTrigger.clearScrollMemory();
        window.history.scrollRestoration = 'manual';
        ScrollTrigger.refresh(true);
    }

    refresh();
    window.addEventListener('resize', refresh);

});

The result is a squashy and squeezy sequence of joy:

Check out the demo

Update: Thanks to Clément’s comment and help on keeping Lenis and GSAP in sync, it’s now much smoother!

And that’s it! Hope you had some fun and that you got some inspiration for your next projects.

Tagged with:

Manoela Ilic

Manoela is the main tinkerer at Codrops. With a background in coding and passion for all things design, she creates web experiments and keeps frontend professionals informed about the latest trends.

Stay up to date with the latest web design and development news and relevant updates from Codrops.