0
0
Vueframework~7 mins

Virtual scrolling for large lists in Vue

Choose your learning style9 modes available
Introduction

Virtual scrolling helps show only a small part of a big list on screen. This makes the app faster and smoother.

When you have a very long list of items to show, like thousands of messages.
When loading all items at once makes the page slow or uses too much memory.
When you want to improve scrolling performance on phones or slow devices.
When you want to save bandwidth by loading only visible items.
When you want a smooth user experience with big data lists.
Syntax
Vue
<template>
  <div ref="scrollContainer" @scroll="onScroll" style="height: 300px; overflow-y: auto;">
    <div :style="{ height: totalHeight + 'px', position: 'relative' }">
      <div
        v-for="item in visibleItems"
        :key="item.id"
        :style="{ position: 'absolute', top: itemPositions[item.index] + 'px', height: itemHeight + 'px', width: '100%' }"
      >
        {{ item.text }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'

const props = defineProps({
  items: Array,
  itemHeight: Number
})

const scrollTop = ref(0)
const scrollContainer = ref(null)

const visibleCount = computed(() => Math.ceil(300 / props.itemHeight) + 1)
const totalHeight = computed(() => props.items.length * props.itemHeight)

const startIndex = computed(() => Math.floor(scrollTop.value / props.itemHeight))

const visibleItems = computed(() => {
  return props.items.slice(startIndex.value, startIndex.value + visibleCount.value).map((item, index) => ({
    ...item,
    index: startIndex.value + index
  }))
})

const itemPositions = computed(() => {
  const positions = {}
  for (let i = startIndex.value; i < startIndex.value + visibleCount.value; i++) {
    positions[i] = i * props.itemHeight
  }
  return positions
})

function onScroll() {
  scrollTop.value = scrollContainer.value.scrollTop
}

onMounted(() => {
  scrollTop.value = 0
})
</script>

The container must have a fixed height and overflow-y set to auto or scroll.

Each item is absolutely positioned inside a tall container to simulate full list height.

Examples
This example shows what happens if the list is empty. No items appear but the container still works.
Vue
<template>
  <div ref="scrollContainer" @scroll="onScroll" style="height: 300px; overflow-y: auto;">
    <div :style="{ height: totalHeight + 'px', position: 'relative' }">
      <div
        v-for="item in visibleItems"
        :key="item.id"
        :style="{ position: 'absolute', top: itemPositions[item.index] + 'px', height: itemHeight + 'px', width: '100%' }"
      >
        {{ item.text }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const items = ref([]) // empty list
const itemHeight = 30

const scrollTop = ref(0)
const scrollContainer = ref(null)

const visibleCount = computed(() => Math.ceil(300 / itemHeight) + 1)
const totalHeight = computed(() => items.value.length * itemHeight)

const startIndex = computed(() => Math.floor(scrollTop.value / itemHeight))

const visibleItems = computed(() => {
  return items.value.slice(startIndex.value, startIndex.value + visibleCount.value).map((item, index) => ({
    ...item,
    index: startIndex.value + index
  }))
})

const itemPositions = computed(() => {
  const positions = {}
  for (let i = startIndex.value; i < startIndex.value + visibleCount.value; i++) {
    positions[i] = i * itemHeight
  }
  return positions
})

function onScroll() {
  scrollTop.value = scrollContainer.value.scrollTop
}
</script>
This example shows a list with only one item. The item is positioned at the top and visible.
Vue
<template>
  <div ref="scrollContainer" @scroll="onScroll" style="height: 300px; overflow-y: auto;">
    <div :style="{ height: totalHeight + 'px', position: 'relative' }">
      <div
        v-for="item in visibleItems"
        :key="item.id"
        :style="{ position: 'absolute', top: itemPositions[item.index] + 'px', height: itemHeight + 'px', width: '100%' }"
      >
        {{ item.text }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const items = ref([{ id: 1, text: 'Only one item' }])
const itemHeight = 30

const scrollTop = ref(0)
const scrollContainer = ref(null)

const visibleCount = computed(() => Math.ceil(300 / itemHeight) + 1)
const totalHeight = computed(() => items.value.length * itemHeight)

const startIndex = computed(() => Math.floor(scrollTop.value / itemHeight))

const visibleItems = computed(() => {
  return items.value.slice(startIndex.value, startIndex.value + visibleCount.value).map((item, index) => ({
    ...item,
    index: startIndex.value + index
  }))
})

const itemPositions = computed(() => {
  const positions = {}
  for (let i = startIndex.value; i < startIndex.value + visibleCount.value; i++) {
    positions[i] = i * itemHeight
  }
  return positions
})

function onScroll() {
  scrollTop.value = scrollContainer.value.scrollTop
}
</script>
This example shows a large list of 1000 items. Only visible items are rendered for smooth scrolling.
Vue
<template>
  <div ref="scrollContainer" @scroll="onScroll" style="height: 300px; overflow-y: auto;">
    <div :style="{ height: totalHeight + 'px', position: 'relative' }">
      <div
        v-for="item in visibleItems"
        :key="item.id"
        :style="{ position: 'absolute', top: itemPositions[item.index] + 'px', height: itemHeight + 'px', width: '100%' }"
      >
        {{ item.text }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const items = ref(Array.from({ length: 1000 }, (_, i) => ({ id: i + 1, text: `Item ${i + 1}` })))
const itemHeight = 30

const scrollTop = ref(0)
const scrollContainer = ref(null)

const visibleCount = computed(() => Math.ceil(300 / itemHeight) + 1)
const totalHeight = computed(() => items.value.length * itemHeight)

const startIndex = computed(() => Math.floor(scrollTop.value / itemHeight))

const visibleItems = computed(() => {
  return items.value.slice(startIndex.value, startIndex.value + visibleCount.value).map((item, index) => ({
    ...item,
    index: startIndex.value + index
  }))
})

const itemPositions = computed(() => {
  const positions = {}
  for (let i = startIndex.value; i < startIndex.value + visibleCount.value; i++) {
    positions[i] = i * itemHeight
  }
  return positions
})

function onScroll() {
  scrollTop.value = scrollContainer.value.scrollTop
}
</script>
Sample Program

This Vue component shows a list of 1000 items using virtual scrolling. Only the items visible in the 300px tall container are rendered. Each item is 40px tall and positioned absolutely inside a tall container. Scrolling updates which items are visible.

Keyboard navigation is supported by adding tabindex and role attributes for accessibility.

Vue
<template>
  <div>
    <h2>Virtual Scrolling List</h2>
    <div ref="scrollContainer" @scroll="onScroll" style="height: 300px; overflow-y: auto; border: 1px solid #ccc;">
      <div :style="{ height: totalHeight + 'px', position: 'relative' }">
        <div
          v-for="item in visibleItems"
          :key="item.id"
          :style="{ position: 'absolute', top: itemPositions[item.index] + 'px', height: itemHeight + 'px', width: '100%', padding: '0.5rem', boxSizing: 'border-box', borderBottom: '1px solid #eee', backgroundColor: item.index % 2 === 0 ? '#fafafa' : '#fff' }"
          tabindex="0"
          role="listitem"
          aria-label="List item"
        >
          {{ item.text }}
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'

const itemHeight = 40
const items = ref(Array.from({ length: 1000 }, (_, i) => ({ id: i + 1, text: `Item number ${i + 1}` })))

const scrollTop = ref(0)
const scrollContainer = ref(null)

const visibleCount = computed(() => Math.ceil(300 / itemHeight) + 1)
const totalHeight = computed(() => items.value.length * itemHeight)

const startIndex = computed(() => Math.floor(scrollTop.value / itemHeight))

const visibleItems = computed(() => {
  return items.value.slice(startIndex.value, startIndex.value + visibleCount.value).map((item, index) => ({
    ...item,
    index: startIndex.value + index
  }))
})

const itemPositions = computed(() => {
  const positions = {}
  for (let i = startIndex.value; i < startIndex.value + visibleCount.value; i++) {
    positions[i] = i * itemHeight
  }
  return positions
})

function onScroll() {
  scrollTop.value = scrollContainer.value.scrollTop
}

onMounted(() => {
  scrollTop.value = 0
})
</script>
OutputSuccess
Important Notes

Virtual scrolling reduces rendering work by only showing visible items, improving speed.

Time complexity for rendering is O(visibleCount), not total items.

Common mistake: forgetting to set container height or overflow, which breaks scrolling.

Use virtual scrolling when lists are very large; for small lists normal rendering is simpler.

Summary

Virtual scrolling shows only visible items in a large list to improve performance.

It uses a fixed height container and absolute positioning inside a tall wrapper.

Scroll events update which items are rendered dynamically.