Skip to content

Virtual Scroller Kit

v0.3.1UI ComponentsVue

Virtual list, table, grid, tree, and select for Vue 3 — dynamic row heights, grouping with animated collapse, sticky headers, infinite scroll, and RTL support.

Virtual Scroller Kit
Get started →
npm install vue-virtual-scroller-kit@latest
01 — Purpose

When you'd reach for this

A plain v-for creates one DOM node per array item, so the difference between 50 rows and 50,000 becomes the difference between "just works" and "the tab hangs the browser" — vue-virtual-scroller-kit keeps only what's actually visible in the DOM.

A data table grows faster than the screen can show it

Thousands of rows, multi-column sorting, pinned rows, and infinite loading as you scroll — only the visible slice ever gets rendered, not the whole table at once.

A long list is split into collapsible sections

Product categories, calendar days, notification groups — both the groups and their contents stay virtualized, and the current section's header stays in view as you scroll through it.

Items need to be reordered by dragging

A playlist, a kanban column, a prioritized task list — items can be dragged and reordered with animated neighbor offsets and auto-scroll, even when only a fraction of the whole list is on screen.

A dropdown searches a huge reference list

A list of countries, tickers, or users running into the thousands — the dropdown itself stays fast through virtualization, and search with on-demand loading replaces rendering every option at once.

02 — Features

At a glance

Virtualisation with dynamic heights and motion blur

Virtualisation with dynamic heights and motion blur

VirtualList and VirtualGrid render only visible elements, supporting lists with a million records. ResizeObserver automatically measures each row’s real height, and the motion blur option adds blur proportional to scroll speed for visual smoothness. Supports horizontal and vertical scrolling, an external container, and window mode.

Grouping, animations, and sticky headers

Grouping, animations, and sticky headers

GroupedVirtualList renders collapsible sections with smooth CSS expand/collapse animations. The stickyGroupHeaders option pins the current group header over the list. The built‑in VirtualScrollbar provides a custom scrollbar with resizable sizes and colours via CSS variables, and RTL support adapts the layout automatically.

Virtual table, grid, tree, and select

Virtual table, grid, tree, and select

VirtualTable with fixed columns, sorting (including multi‑), resizable and reorderable columns, pinned rows, and infinite scrolling. VirtualTree supports lazy loading of children with configurable indentation. VirtualSelect virtualises the dropdown with search, async loading, and custom slots.

Composables for keyboard, drag‑to‑reorder, and selection

Composables for keyboard, drag‑to‑reorder, and selection

useVirtualKeyboardNav adds arrow‑key navigation, Home/End, PageUp/Down with focus and Enter activation. useDraggableList implements drag‑to‑reorder with animated neighbour offsets, a ghost element, and auto‑scroll. useRowSelection manages row selection with Shift+click support, select‑all, and clearing.

SSR safety, performance, and fine‑grained control

SSR safety, performance, and fine‑grained control

Components are SSR‑safe: the first N rows are rendered on the server, and hydration happens without layout shift. The PositionManager with a segment tree provides O(log n) updates and queries. Scroll‑position restoration, anchor compensation on row height changes, a DOM recycling pool, and a custom scrollbar — all for maximum performance.

Infinite loading and visibility tracking

Infinite loading and visibility tracking

The InfiniteLoader component fetches the next page shortly before the user scrolls to the end of the list, hiding network latency behind a smooth scroll. The useVisibilityTracker composable reports which items are actually within the viewport — handy for impression analytics. VirtualTable’s autoColWidths sizes columns to fit row content automatically.

03 — Quick example

See how it works

A million rows, no FPS drop

Only the visible items plus a small overscan buffer actually render, and row heights are dynamic, re-measured on the fly.

App.vue
<script setup lang="ts">
import { VirtualList } from 'vue-virtual-scroller-kit'

interface Row {
  id: number
  text: string
}

const items: Row[] = Array.from({ length: 100_000 }, (_, i) => ({
  id: i,
  text: `Row ${i + 1}`,
}))
</script>

<template>
  <VirtualList :items="items" :estimated-item-size="48" style="height: 600px">
    <template #default="{ item, index }">
      <div style="padding: 12px 16px; border-bottom: 1px solid #eee">
        {{ index + 1 }}. {{ item.text }}
      </div>
    </template>
  </VirtualList>
</template>

Groups with sticky headers and collapsing

GroupedVirtualList virtualizes a list inside collapsible sections — the current group's header stays pinned at the top, and toggle() expands or collapses any group programmatically.

GroupedList.vue
<script setup lang="ts">
import { ref } from 'vue'
import { GroupedVirtualList } from 'vue-virtual-scroller-kit'
import type { GroupDef, GroupedVirtualListExpose } from 'vue-virtual-scroller-kit'

interface Contact {
  id: number
  name: string
  email: string
}

const groups = ref<GroupDef<Contact>[]>([
  {
    key: 'a',
    label: 'A',
    items: [
      { id: 1, name: 'Alice', email: 'alice@example.com' },
      { id: 2, name: 'Aaron', email: 'aaron@example.com' },
    ],
  },
  {
    key: 'b',
    label: 'B',
    items: [{ id: 3, name: 'Bob', email: 'bob@example.com' }],
    collapsed: true,
  },
])

const listRef = ref<GroupedVirtualListExpose | null>(null)
</script>

<template>
  <GroupedVirtualList
    ref="listRef"
    :groups="groups"
    :estimated-item-size="56"
    sticky-group-headers
    style="height: 500px"
  >
    <template #group-header="{ group, toggle, isCollapsed }">
      <div class="group-header" @click="toggle">
        {{ isCollapsed ? '▶' : '▼' }} {{ group.label }}
        <span>({{ group.items.length }})</span>
      </div>
    </template>

    <template #default="{ item }">
      <div class="contact-row">
        <strong>{{ item.name }}</strong>
        <span>{{ item.email }}</span>
      </div>
    </template>
  </GroupedVirtualList>
</template>

Drag-to-reorder without a single external library

useDraggableList tracks the ghost element under the cursor, animates neighboring rows out of the way, and auto-scrolls the container near its edges — you just spread getItemProps across the markup.

DraggableList.vue
<script setup lang="ts">
import { ref } from 'vue'
import { useDraggableList } from 'vue-virtual-scroller-kit'

interface Card {
  id: number
  label: string
}

const cards = ref<Card[]>(Array.from({ length: 50 }, (_, i) => ({ id: i, label: `Card ${i + 1}` })))
const listRef = ref<HTMLElement | null>(null)

const { isDragging, dragIndex, ghostStyle, getItemStyle, getItemProps } = useDraggableList({
  items: cards,
  scrollContainer: listRef,
  onReorder: (newItems) => {
    cards.value = newItems
  },
})
</script>

<template>
  <div ref="listRef" style="display: flex; flex-direction: column; gap: 6px; overflow-y: auto; height: 500px">
    <div
      v-for="(card, index) in cards"
      :key="card.id"
      v-bind="getItemProps(index)"
      :style="getItemStyle(index)"
      class="card"
    >
      ⣿ {{ card.label }}
    </div>
  </div>

  <Teleport to="body">
    <div v-if="isDragging && dragIndex >= 0" class="card card--ghost" :style="ghostStyle">
      ⣿ {{ cards[dragIndex]?.label }}
    </div>
  </Teleport>
</template>