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.

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.