Skip to content

Virtual Scroll

A virtual scrolling component wrapped around @tanstack/vue-virtual, designed for high-performance scrolling scenarios with large data lists. Only elements within the visible area are rendered, significantly improving rendering performance for long lists.

Note: This component depends on @tanstack/vue-virtual via dynamic import as an optional dependency. If not installed, the component displays a prompt message instead of throwing an error. Install with: pnpm add @tanstack/vue-virtual

Demo

Preview

基础用法(5000 条数据)

斑马纹列表(striped)

带边框列表(bordered)

滚动到指定索引(scrollToIndex)

Installation

pnpm dlx brutx-vue@latest add virtual-scroll

Usage

vue
<script setup>
import { VirtualScroll } from 'brutx-ui-vue'

const items = Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `Item ${i + 1}`,
}))
</script>

<template>
    <VirtualScroll :items="items" :item-height="48">
        <template #default="{ item, index }">
            <div class="p-4 border-b-2 border-brutal">
                {{ item.name }}
            </div>
        </template>
    </VirtualScroll>
</template>

Striped List

vue
<VirtualScroll
    :items="items"
    variant="striped"
    size="lg"
>
    <template #default="{ item }">
        <div class="p-4">{{ item.name }}</div>
    </template>
</VirtualScroll>

Load More

vue
<VirtualScroll
    :items="items"
    @scroll-end="loadMore"
>
    <template #default="{ item }">
        <div class="p-4">{{ item.name }}</div>
    </template>
    <template #loading>
        <div class="p-4 text-center">Loading...</div>
    </template>
</VirtualScroll>

Empty State

vue
<VirtualScroll :items="[]">
    <template #empty>
        <div class="p-8 text-center">
            <Icon name="empty" class="w-16 h-16 mx-auto mb-4" />
            <p>No data available</p>
        </div>
    </template>
</VirtualScroll>

Bordered List

vue
<VirtualScroll
    :items="items"
    variant="bordered"
    :item-height="64"
>
    <template #default="{ item }">
        <div class="flex items-center gap-4 p-4">
            <Avatar :src="item.avatar" />
            <div>
                <p class="font-bold">{{ item.name }}</p>
                <p class="text-sm text-muted">{{ item.email }}</p>
            </div>
        </div>
    </template>
</VirtualScroll>

Variants

VariantDescription
defaultDefault style
stripedZebra striping (even row background color)
borderedBottom border

Sizes

SizeMax Height
smmax-h-64 (16rem)
defaultmax-h-96 (24rem)
lgmax-h-[32rem]
xlmax-h-[48rem]
fullmax-h-full

Data Types

ts
interface VirtualScrollItem {
    id: string | number
    [key: string]: unknown
}

Programmatic Control

After referencing the component via ref, you can call the following methods (exposed via defineExpose):

vue
<script setup>
import { ref } from 'vue'
import { VirtualScroll } from 'brutx-ui-vue'

const listRef = ref(null)
const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i + 1}` }))

function jumpToMiddle() {
    listRef.value?.scrollToIndex(Math.floor(items.length / 2))
}
</script>

<template>
    <Button @click="jumpToMiddle">Jump to Middle</Button>
    <VirtualScroll ref="listRef" :items="items" :item-height="48">
        <template #default="{ item }">
            <div class="p-4">{{ item.name }}</div>
        </template>
    </VirtualScroll>
</template>

Exposed API

MethodParameterDescription
scrollToIndexindex: numberScroll to the list item at the specified index

Props

PropTypeDefaultDescription
itemsVirtualScrollItem[]Data array (required), each item must have an id field
itemHeightnumber48Height of each item (in pixels)
size'sm' | 'default' | 'lg' | 'xl' | 'full''default'Container size variant
variant'default' | 'striped' | 'bordered''default'List item style variant
overscannumber5Number of items to pre-render outside the visible area
scrollEndThresholdnumber50Scroll-to-bottom detection threshold (in pixels)
classstringExternal CSS class override

Events

EventPayloadDescription
scrollscrollTop: numberFired on scroll
scroll-endFired when scrolled to the bottom

Slots

SlotScopeDescription
default{ item: VirtualScrollItem, index: number }List item rendering
emptyEmpty state display (shown when items is an empty array)
loadingLoad more display (only rendered when this slot is provided)

Accessibility

  • ARIA Attributes: Uses role="list" and role="listitem" for semantic markup; supports aria-setsize and aria-posinset attributes
  • Keyboard: Supports aria-label for internationalized labels

FAQ

Q: What should I do when list items have variable heights and the virtual scroll jumps around?

A: itemHeight is an estimated value used to calculate scroll position. If list items vary significantly in height, it may cause jumping during scrolling. It is recommended to set itemHeight to the average height of most list items, or ensure consistent heights via CSS. Increasing the overscan value can also help reduce the jumping effect.

Q: The component shows a prompt message instead of rendering normally after installation. Why?

A: VirtualScroll depends on @tanstack/vue-virtual, which is an optional dependency. If the package is not installed, the component displays an installation prompt instead of throwing an error. Run pnpm add @tanstack/vue-virtual to install the dependency and restart your project.

Q: The scroll-end event is not firing when I scroll to the bottom?

A: Please check whether the data source is provided correctly. The scroll-end event only fires when the container actually scrolls and reaches the bottom. If the data volume is insufficient to produce a scrollbar (the list height does not exceed the container), the event will not fire. You can adjust the scrollEndThreshold value to control the trigger sensitivity.

Brute force builds.