Виртуальные список, таблица, грид, дерево и select для Vue 3 — динамические высоты строк, группировка с анимацией, sticky-заголовки, бесконечная прокрутка, поддержка RTL.

Обычный v-for создаёт DOM-узел на каждый элемент массива, и разница между 50 и 50 000 строк становится разницей между «просто работает» и «браузер вешает вкладку», а vue-virtual-scroller-kit держит в DOM только то, что реально видно на экране.
Тысячи строк, сортировка сразу по нескольким колонкам, закреплённые ряды и подгрузка по мере скролла — в браузере при этом отрисовывается только видимая часть таблицы, а не она целиком.
Категории товаров, дни в календаре, группы уведомлений — виртуализируются сразу и сами группы, и их содержимое, а заголовок текущей группы остаётся на виду, пока листаешь список внутри неё.
Плейлист, канбан-колонка, список приоритетных задач — элементы можно перетаскивать и переставлять с анимацией соседей и автопрокруткой, даже когда на экране видна лишь тысячная доля всего списка.
Список стран, тикеров или пользователей на тысячи записей — сам выпадающий список остаётся быстрым за счёт виртуализации, а поиск с подгрузкой результатов заменяет отрисовку всех вариантов разом.

VirtualList и VirtualGrid рендерят только видимые элементы, поддерживая списки с миллионом записей. ResizeObserver автоматически измеряет реальную высоту каждой строки, а опция motion blur добавляет размытие, пропорциональное скорости прокрутки, для визуальной плавности. Поддерживаются горизонтальная и вертикальная прокрутка, внешний контейнер и оконный режим.

GroupedVirtualList отображает секции с коллапсируемыми группами и плавными CSS-анимациями раскрытия/схлопывания. Опция stickyGroupHeaders фиксирует заголовок текущей группы поверх списка. Встроенный VirtualScrollbar даёт кастомный скроллбар с изменяемыми размерами и цветами через CSS-переменные, а RTL-поддержка адаптирует макет автоматически.

VirtualTable с фиксированными колонками, сортировкой (в том числе мульти-), изменяемым размером и порядком колонок, закреплёнными рядами и бесконечной подгрузкой. VirtualTree поддерживает ленивую загрузку дочерних узлов с настраиваемым отступом. VirtualSelect виртуализирует выпадающий список с поиском, асинхронной загрузкой и кастомными слотами.

useVirtualKeyboardNav добавляет навигацию стрелками, Home/End, PageUp/Down с фокусом и активацией по Enter. useDraggableList реализует drag‑to‑reorder с анимированным смещением соседних элементов, призрачной копией и автоскроллом. useRowSelection управляет выбором строк с поддержкой Shift+клик, полным выделением и сбросом.

Компоненты безопасны для SSR: первые N строк рендерятся на сервере, а гидратация происходит без скачка. PositionManager с сегментным деревом обеспечивает O(log n) обновлений и запросов. Восстановление позиции прокрутки, anchor‑компенсация при изменении высоты строк, пул переиспользуемых DOM-узлов и кастомный скроллбар — всё для максимальной производительности.

Компонент InfiniteLoader подгружает следующую страницу данных незадолго до конца списка, скрывая задержку сети за плавным скроллом. Композабл useVisibilityTracker сообщает, какие элементы реально находятся в области просмотра — удобно для аналитики показов. Опция autoColWidths у VirtualTable сама подбирает ширину колонок по содержимому строк.
Рендерятся только видимые элементы плюс небольшой запас (overscan), высоты — динамические и переизмеряются на лету.
<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>GroupedVirtualList виртуализирует список внутри секций — заголовок текущей группы остаётся закреплённым сверху, а toggle() сворачивает или разворачивает любую из них программно.
<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>useDraggableList сам считает призрак-элемент под курсором, анимирует соседей и автоскроллит контейнер у края — остаётся только расставить getItemProps по вёрстке.
<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>