Vue Example: Smooth Scroll

vue
<template>
  <div>
    <p>
      This smooth scroll example uses the <code>`scrollToFn`</code> to implement
      a custom scrolling function for the methods like
      <code>`scrollToIndex`</code> and <code>`scrollToOffset`</code>
    </p>

    <br />
    <br />

    <div>
      <button
        @click="
          (rowVirtualizer.scrollToIndex(randomIndex),
          (randomIndex = generateRandomIndex()))
        "
      >
        Scroll To Random Index ({{ randomIndex }})
      </button>
    </div>

    <br />
    <br />

    <div
      ref="parentRef"
      class="List"
      :style="{
        height: `200px`,
        width: `400px`,
        overflow: 'auto',
      }"
    >
      <div
        :style="{
          height: `${totalSize}px`,
          width: '100%',
          position: 'relative',
        }"
      >
        <div
          v-for="virtualRow in virtualRows"
          :key="virtualRow.key"
          :class="virtualRow.index % 2 ? 'ListItemOdd' : 'ListItemEven'"
          :style="{
            position: 'absolute',
            top: 0,
            left: 0,
            width: '100%',
            height: `${virtualRow.size}px`,
            transform: `translateY(${virtualRow.start}px)`,
          }"
        >
          Row {{ virtualRow.index }}
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import {
  elementScroll,
  useVirtualizer,
  VirtualizerOptions,
} from '@tanstack/vue-virtual'

const easeInOutQuint = (t: number) => {
  return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t
}

const generateRandomIndex = () => Math.floor(Math.random() * 10000)

const randomIndex = ref(generateRandomIndex())

const parentRef = ref<HTMLElement | null>(null)
const scrollingRef = ref<number>()

const scrollToFn: VirtualizerOptions<any, any>['scrollToFn'] = (
  offset,
  canSmooth,
  instance,
) => {
  const duration = 1000
  const start = parentRef.value?.scrollTop || 0
  const startTime = (scrollingRef.value = Date.now())

  const run = () => {
    if (scrollingRef.value !== startTime) return
    const now = Date.now()
    const elapsed = now - startTime
    const progress = easeInOutQuint(Math.min(elapsed / duration, 1))
    const interpolated = start + (offset - start) * progress

    if (elapsed < duration) {
      elementScroll(interpolated, canSmooth, instance)
      requestAnimationFrame(run)
    } else {
      elementScroll(interpolated, canSmooth, instance)
    }
  }

  requestAnimationFrame(run)
}

const rowVirtualizerOptions = computed(() => {
  return {
    count: 10000,
    getScrollElement: () => parentRef.value,
    estimateSize: () => 35,
    overscan: 5,
    scrollToFn,
  }
})

const rowVirtualizer = useVirtualizer(rowVirtualizerOptions)

const virtualRows = computed(() => rowVirtualizer.value.getVirtualItems())

const totalSize = computed(() => rowVirtualizer.value.getTotalSize())
</script>
<template>
  <div>
    <p>
      This smooth scroll example uses the <code>`scrollToFn`</code> to implement
      a custom scrolling function for the methods like
      <code>`scrollToIndex`</code> and <code>`scrollToOffset`</code>
    </p>

    <br />
    <br />

    <div>
      <button
        @click="
          (rowVirtualizer.scrollToIndex(randomIndex),
          (randomIndex = generateRandomIndex()))
        "
      >
        Scroll To Random Index ({{ randomIndex }})
      </button>
    </div>

    <br />
    <br />

    <div
      ref="parentRef"
      class="List"
      :style="{
        height: `200px`,
        width: `400px`,
        overflow: 'auto',
      }"
    >
      <div
        :style="{
          height: `${totalSize}px`,
          width: '100%',
          position: 'relative',
        }"
      >
        <div
          v-for="virtualRow in virtualRows"
          :key="virtualRow.key"
          :class="virtualRow.index % 2 ? 'ListItemOdd' : 'ListItemEven'"
          :style="{
            position: 'absolute',
            top: 0,
            left: 0,
            width: '100%',
            height: `${virtualRow.size}px`,
            transform: `translateY(${virtualRow.start}px)`,
          }"
        >
          Row {{ virtualRow.index }}
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import {
  elementScroll,
  useVirtualizer,
  VirtualizerOptions,
} from '@tanstack/vue-virtual'

const easeInOutQuint = (t: number) => {
  return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t
}

const generateRandomIndex = () => Math.floor(Math.random() * 10000)

const randomIndex = ref(generateRandomIndex())

const parentRef = ref<HTMLElement | null>(null)
const scrollingRef = ref<number>()

const scrollToFn: VirtualizerOptions<any, any>['scrollToFn'] = (
  offset,
  canSmooth,
  instance,
) => {
  const duration = 1000
  const start = parentRef.value?.scrollTop || 0
  const startTime = (scrollingRef.value = Date.now())

  const run = () => {
    if (scrollingRef.value !== startTime) return
    const now = Date.now()
    const elapsed = now - startTime
    const progress = easeInOutQuint(Math.min(elapsed / duration, 1))
    const interpolated = start + (offset - start) * progress

    if (elapsed < duration) {
      elementScroll(interpolated, canSmooth, instance)
      requestAnimationFrame(run)
    } else {
      elementScroll(interpolated, canSmooth, instance)
    }
  }

  requestAnimationFrame(run)
}

const rowVirtualizerOptions = computed(() => {
  return {
    count: 10000,
    getScrollElement: () => parentRef.value,
    estimateSize: () => 35,
    overscan: 5,
    scrollToFn,
  }
})

const rowVirtualizer = useVirtualizer(rowVirtualizerOptions)

const virtualRows = computed(() => rowVirtualizer.value.getVirtualItems())

const totalSize = computed(() => rowVirtualizer.value.getTotalSize())
</script>
Subscribe to Bytes

Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.

Bytes

No spam. Unsubscribe at any time.