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 (requires parent container to have a definite height, e.g. h-screen/h-96)

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>

API Reference ​

VirtualScroll — Props
NameTypeDefaultDescription
class
string | undefined
undefined
Custom CSS classes appended to the virtual scroll root container.
Details

Declared type

string | undefined

Default value details

Resolved value
undefined

Source

types.ts:22

packages/ui/src/components/virtual-scroll/types.ts

items
Required
T[]
—
The data array to virtualize. Its length determines the item count in the virtualizer.
Details

Declared type

T[]

Default value details

Not declared
—

Source

types.ts:8

packages/ui/src/components/virtual-scroll/types.ts

itemHeight
number | undefined
48
The estimated item height in pixels for fixed-height mode; dynamic-height mode also uses it as the initial estimate.
Details

Declared type

number | undefined

Default value details

Resolved value
48

Source

types.ts:10

packages/ui/src/components/virtual-scroll/types.ts

dynamicHeight
boolean | undefined
false
Enables measurement of mounted items’ actual heights. When false, each item uses the fixed itemHeight.
Details

Declared type

boolean | undefined

Literal values

  • false
  • true

Default value details

Resolved value
false

Source

types.ts:12

packages/ui/src/components/virtual-scroll/types.ts

size
"default" | "lg" | "sm" | "xl" | "full" | undefined
"default"
The container size variant. The full variant requires a parent with a definite height.
Details

Declared type

"default" | "lg" | "sm" | "xl" | "full" | undefined

Literal values

  • "default"
  • "full"
  • "lg"
  • "sm"
  • "xl"

Default value details

Resolved value
"default"

Source

types.ts:14

packages/ui/src/components/virtual-scroll/types.ts

variant
"default" | "bordered" | "striped" | undefined
"default"
Selects the item style. Striped adds an alternating background to odd zero-based indices; bordered adds borders.
Details

Declared type

"default" | "bordered" | "striped" | undefined

Literal values

  • "bordered"
  • "default"
  • "striped"

Default value details

Resolved value
"default"

Source

types.ts:16

packages/ui/src/components/virtual-scroll/types.ts

overscan
number | undefined
5
The number of extra items rendered beyond the visible region, passed to the virtualizer.
Details

Declared type

number | undefined

Default value details

Resolved value
5

Source

types.ts:18

packages/ui/src/components/virtual-scroll/types.ts

scrollEndThreshold
number | undefined
50
The pixel distance from the bottom at which scroll-end is emitted.
Details

Declared type

number | undefined

Default value details

Resolved value
50

Source

types.ts:20

packages/ui/src/components/virtual-scroll/types.ts

role
string | undefined
"list"
Sets the ARIA role on the virtual list scroll container; defaults to list.
Details

Declared type

string | undefined

Default value details

Resolved value
"list"

Source

types.ts:24

packages/ui/src/components/virtual-scroll/types.ts

itemRole
string | undefined
"listitem"
Sets the ARIA role on each virtual list item; defaults to listitem.
Details

Declared type

string | undefined

Default value details

Resolved value
"listitem"

Source

types.ts:26

packages/ui/src/components/virtual-scroll/types.ts

VirtualScroll — Events
NameSignature or typeDescription
@scroll-end
[]
Emitted when scrolling enters the bottom range set by scrollEndThreshold. It can fire again after leaving and re-entering that range.
Details

Declared type

[]

Source

VirtualScroll.vue

packages/ui/src/components/virtual-scroll/VirtualScroll.vue

@scroll
[scrollTop: number]
Emitted with the current scrollTop in pixels when the scroll container scrolls.
Details

Declared type

[scrollTop: number]

Source

VirtualScroll.vue

packages/ui/src/components/virtual-scroll/VirtualScroll.vue

VirtualScroll — Slots
NameSignature or typeDescription
#loading
{}
Provides loading content. It renders while the virtualizer dependency loads and at the bottom of a ready, non-empty list.
Details

Declared type

{}

Source

VirtualScroll.vue

packages/ui/src/components/virtual-scroll/VirtualScroll.vue

#empty
{}
Replaces the empty-state message and is rendered only in the empty-list branch.
  • Without slot content, the component displays virtualScroll.empty: “No data available”.
Details

Declared type

{}

Source

VirtualScroll.vue

packages/ui/src/components/virtual-scroll/VirtualScroll.vue

#default
{ item: T; index: number; }
Renders each visible virtual item. Its scope provides the original item and its zero-based index.
Details

Declared type

{ item: T; index: number; }

Source

VirtualScroll.vue

packages/ui/src/components/virtual-scroll/VirtualScroll.vue

VirtualScroll — Exposes
NameSignature or typeDescription
scrollToIndex
(index: number) => void
Scrolls to an item index. The index is clamped to the available range; it does nothing for an empty list or before the virtualizer is available.
Details

Declared type

(index: number) => void

Source

VirtualScroll.vue

packages/ui/src/components/virtual-scroll/VirtualScroll.vue

measureElement
(el: Element | null) => void
Passes the specified DOM element to the virtualizer for measurement. It does nothing for null or before the virtualizer is ready.
Details

Declared type

(el: Element | null) => void

Source

VirtualScroll.vue

packages/ui/src/components/virtual-scroll/VirtualScroll.vue

measure
() => void
Re-measures all mounted item sizes, for use after content heights change.
Details

Declared type

() => void

Source

VirtualScroll.vue

packages/ui/src/components/virtual-scroll/VirtualScroll.vue

virtualizer
ShallowRef<VirtualizerInstance | null, VirtualizerInstance | null>
The shallow reactive reference to the underlying TanStack Virtualizer. Its value is null until the dependency loads, and remains null if loading fails.
Details

Declared type

ShallowRef<VirtualizerInstance | null, VirtualizerInstance | null>

Literal values

  • null

Referenced types

VirtualizerInstance
Type definition — VirtualizerInstance
export interface VirtualizerInstance {
    getVirtualItems: () => VirtualizerVirtualItem[]
    getTotalSize: () => number
    scrollToIndex: (index: number) => void
    measure: () => void
    measureElement: (el: Element | null) => void
}

Source

VirtualScroll.vue

packages/ui/src/components/virtual-scroll/VirtualScroll.vue

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.