vue-virtual-scroller-kit
Virtual Scroller Kit

Virtual list, table, grid, tree, and select for Vue 3. Dynamic row heights measured by ResizeObserver, grouping with animated expand/collapse, sticky headers, infinite scroll, keyboard navigation, drag-to-reorder, RTL support, and full SSR support — all with a single peer dependency (Vue 3).
Features
- Dynamic row heights —
ResizeObservermeasures each row after render; position manager updates in O(log n) - VirtualList — flat list, external scroll container, page-mode (window scroll), DOM recycling pool, scroll restoration, opt-in
horizontallayout - GroupedVirtualList — collapsible sections with smooth expand/collapse CSS animation, opt-in sticky group-header overlay
- VirtualTable — native
<table>with spacer-row virtual scroll, sticky header, fixed left/right columns, single and multi-column sort, drag-to-resize and drag-to-reorder columns, column show/hide, column virtualization, pinned top/bottom rows, built-in lazy loading - VirtualGrid — fixed-height cells in a responsive auto-column or fixed-column grid, or opt-in per-row dynamic height
- VirtualTree — hierarchical expand/collapse with lazy child loading, configurable indent
- InfiniteLoader — trigger
onLoadMorenear the bottom, top, or both ends; scroll-position preservation when prepending - VirtualSelect — virtualized dropdown with client-side or async/remote search (debounced, with a loading slot) and keyboard navigation
- VirtualScrollbar — themable custom scrollbar overlay that syncs with any of the components above (or any scrollable element)
- Smooth programmatic scrolling —
scrollTo/scrollToOffsetaccept{ behavior: 'smooth' }, using the browser's native smooth-scroll - Motion blur — opt-in
motionBlurprop applies a velocity-scaled CSS blur while scrolling fast, clearing once scrolling settles - Anchor-compensated reflow — when a row above the viewport is measured to a different height, scroll position is adjusted by the same delta so visible content never jumps
- RTL support — CSS logical properties throughout, plus
scrollLeftnormalization for column virtualization, horizontalVirtualList, and the scrollbar - useVirtualScroll — raw composable for custom containers; returns
visibleRange,totalHeight,scrollTo; supports ahorizontalaxis - useVirtualKeyboardNav — arrow keys, Home/End, PageUp/PageDown, Enter/Space; plugs into any virtual list
- useDraggableList — pointer-event drag-to-reorder with animated gap, ghost element, auto-scroll, disabled-item support
- useRowSelection — dataset-agnostic click/Shift-click row selection (single or multi), pairs with
VirtualListorVirtualTable - useVisibilityTracker — per-key enter/leave-viewport events backed by a real
IntersectionObserver - PositionManager — segment tree (O(log n) updates and prefix-sum queries) exposed for advanced use
- autoColWidths — estimates column widths from a data sample via Canvas
measureText; SSR-safe fallback - SSR-safe — first N rows rendered on the server, hydration without layout shift
- Zero external dependencies — only Vue 3 as peer dep
- Tree-shakeable ESM + CJS dual build
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.
Installation
Requires Vue 3.3+. No other runtime dependencies — the package has zero dependencies of its own.
npm install vue-virtual-scroller-kit
Import the stylesheet once, anywhere in your app entry — it styles things the
inline :style bindings don't cover (table column resize/pin, tree toggle
buttons, the select dropdown, the custom scrollbar thumb, and skeleton
loading states):
import 'vue-virtual-scroller-kit/style.css'
Peer dependency:
npm install vue@>=3.3
Quick start
<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>
More examples
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.
<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.
<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>
Documentation & links
- 📖 Full documentation: npm.vuecraft.ru/en/packages/vue-virtual-scroller-kit
- 🌐 VueCraft: vuecraft.ru/en
- 👤 Author: macrulez.ru/en
- 💻 GitHub: macrulezru/vue-virtual-scroller-kit
- 📦 NPM: vue-virtual-scroller-kit
- 🐛 Issues: github.com/macrulezru/vue-virtual-scroller-kit/issues
License
MIT
💖 Support the project
Open source takes time and effort. If this library saves you time or brings value, consider supporting further development.
Thank you for being part of this journey. ❤️
Related Packages
Virtual scrolling for Vue 3: only the rows near the viewport are in the DOM.
A high-performance virtualized list component for Vue 3 that efficiently renders...
High-performance virtualized data grid for Vue 3, with DOM node recycling, cell...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.3.1 | >=3.3.0 | MIT | |
| 0.3.0 | >=3.3.0 | MIT | |
| 0.2.6 | >=3.3.0 | MIT | |
| 0.2.5 | >=3.3.0 | MIT | |
| 0.2.4 | >=3.3.0 | MIT | |
| 0.2.3 | >=3.3.0 | MIT | |
| 0.2.2 | >=3.3.0 | MIT | |
| 0.2.1 | >=3.3.0 | MIT | |
| 0.2.0 | >=3.3.0 | MIT | |
| 0.1.8 | >=3.3.0 | MIT | |
| 0.1.7 | >=3.3.0 | MIT | |
| 0.1.6 | >=3.3.0 | MIT | |
| 0.1.5 | >=3.3.0 | MIT | |
| 0.1.4 | >=3.3.0 | MIT | |
| 0.1.3 | >=3.3.0 | MIT | |
| 0.1.2 | >=3.3.0 | MIT | |
| 0.1.0 | >=3.3.0 | MIT |