Frontend Development 14 min read

Building a Seamless Infinite Scroll Component with Vue 3

This article explains why infinite scrolling is essential for large‑screen dashboards, analyses three implementation strategies, and provides a complete Vue 3 component—including BEM‑styled markup, GSAP‑driven animation, slot‑based content insertion, and optional hover‑pause behavior—to achieve a smooth, endless scrolling list.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Building a Seamless Infinite Scroll Component with Vue 3

Large‑screen dashboards often need a scrolling list that appears continuous, even when the data source is static or lacks a WebSocket connection; the illusion of real‑time data helps tell a compelling story to stakeholders.

Standard lists have three major drawbacks: they have an end, their scrolling feels flat, and the transition between the last and first items is jarring. To solve these issues, the article defines three core requirements for an infinite scroll component: each item should scroll up by one unit in N seconds, pause for M seconds for readability, and loop back to the first item seamlessly.

Idea A – Reorder Elements : Adjust the order of the original list during scrolling. This fails when the viewport is slightly smaller than the list because an element would need to appear simultaneously at the top and bottom.

Idea B – Clone Nodes : Duplicate elements using Node.cloneNode() . This approach loses dynamically bound events (e.g., those added via addEventListener ) and complicates integration with virtual‑DOM frameworks such as React or Vue.

Idea C – Duplicate List (Double‑Copy) : Render two identical lists stacked vertically. When the first list scrolls out of view, it is instantly repositioned to the top, creating a seamless loop without reordering or cloning individual nodes. This method is simple, avoids event‑loss issues, and works well with component frameworks.

The implementation uses Vue 3 and GSAP. The component follows BEM naming ( .seamless-scroll , .seamless-scroll__wrapper , .seamless-scroll__box‑top , .seamless-scroll__box‑bottom ) and provides a default slot for user‑supplied list items.

{
  "dependencies": {
    "gsap": "latest", // animation library
    "@vueuse/core": "latest" // Vue composition utilities
  }
}

Props are defined with defineProps to control delay (pause between scrolls) and duration (time to scroll one item):

const props = defineProps({
  /** pause between two scroll steps */
  delay: { type: Number, default: 1 },
  /** time to move one item */
  duration: { type: Number, default: 2 }
})

The template consists of a wrapper, a box, and two identical slot containers ( .seamless-scroll__box‑top and .seamless-scroll__box‑bottom ) that render the same content:

<template>
  <div class="seamless-scroll">
    <div ref="wrapperRef" class="seamless-scroll__wrapper">
      <div ref="boxRef" class="seamless-scroll__box">
        <div class="seamless-scroll__box-top" ref="topRef">
          <slot></slot>
        </div>
        <div class="seamless-scroll__box-bottom">
          <slot></slot>
        </div>
      </div>
    </div>
  </div>
</template>

CSS (SCSS) hides the native scrollbar and ensures the wrapper fills its parent:

.seamless-scroll {
  &__wrapper { width: 100%; height: 100%; position: relative; overflow: hidden; }
  &__box { &-top, &-bottom { overflow: hidden; } }
}

GSAP creates a timeline that animates wrapperRef.scrollTop to the calculated target position, using the duration prop and a delay of props.delay seconds:

import gsap from 'gsap'

onMounted(() => {
  const timeLine = gsap.timeline()
  timeLine.to(wrapperRef.value, { scrollTop: 200, duration: props.duration }, `+=${props.delay}`)
})

To achieve per‑item scrolling, the component extracts the child elements of the top slot, filters out text nodes, and records the current index ( scrollingElIndex ). For each step it computes the element’s height via getBoundingClientRect() , calculates the next scrollTop value, and animates to it.

const nodeList = topRef.value.childNodes
const nodeArr = Array.from(nodeList).filter(t => t.nodeType === Node.ELEMENT_NODE)
let scrollingElIndex = 0
const current = nodeArr[scrollingElIndex]
scrollingElIndex = (scrollingElIndex + 1) % nodeArr.length
const { height } = current.getBoundingClientRect()
const target = current.offsetTop + height
// animate to target

When the index wraps back to 0 , the wrapper is instantly reset to scrollTop = 0 (duration 0) before starting the next animation, creating the illusion of an endless list.

if (scrollingElIndex === 0) {
  gsap.to(wrapperRef.value, { scrollTop: 0, duration: 0, onComplete: genAnimates })
}

Additional features include: (1) disabling scrolling when the list height is too small, (2) pausing the GSAP timeline on mouse hover and resuming on mouse leave, and (3) adding odd/even state classes for zebra‑striping.

const onMouseOver = () => timeLine.pause()
const onMouseOut = () => timeLine.resume()

The article concludes with links to a VitePress‑based demo and the full source repository, encouraging readers to either adopt an existing library or build their own component following the presented methodology.

frontendComponent DesignInfinite ScrollVue3GSAPBEM
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.