Naive UI Virtual List Component
When it comes to virtual lists, it can feel like a list is infinitely long, but in reality it just secretly hides the invisible elements. Perfect for rendering large datasets efficiently.
When to Use
Use this component when:
- Large lists: Render lists with thousands of items efficiently
- Chat messages: Display large message histories
- Data tables: Show large datasets without performance issues
- Infinite scroll: Implement virtual scrolling for better performance
When to Invoke
Invoke this skill when:
- User needs to render large lists efficiently
- User wants to implement virtual scrolling
- User needs dynamic item sizes
- User wants to scroll to specific items programmatically
- User asks about performance optimization for large lists
Features
- Virtual Rendering: Only renders visible items for performance
- Fixed or Dynamic Size: Support for fixed or dynamic item heights
- Programmatic Scroll: Scroll to index, key, or position
- Keep-alive Support: Preserve scroll state with keep-alive
- Resizable Items: Dynamic item size calculation
- Scrollbar Integration: Built-in scrollbar support
API Reference
Virtual List Props
| Name | Type | Default | Description |
|---|---|---|---|
| default-scroll-key | string | number |
undefined |
Default scroll key. |
| default-scroll-index | number |
undefined |
Default scroll index. |
| ignore-item-resize | boolean |
false |
Ignore item resize for slightly better performance. |
| items | Array<object> |
[] |
Data to display. |
| item-resizable | boolean |
false |
Whether dynamic sizing is enabled. |
| item-size | number |
required | Minimum height of the item in pixels. |
| items-style | string | CSSProperties |
undefined |
Items container style. |
| key-field | string |
'key' |
Field name of option key. |
| padding-top | string | number |
undefined |
Distance from the top. |
| padding-bottom | string | number |
undefined |
Distance from the bottom. |
| scrollbar-props | ScrollbarProps |
undefined |
Scrollbar props. |
| visible-items-tag | string |
'div' |
Items container tag. |
| visible-items-props | object |
undefined |
Items container props. |
| on-scroll | (event: Event) => void |
undefined |
Scrolling callback function. |
| on-wheel | (event: WheelEvent) => void |
undefined |
Callback for wheel event. |
| on-resize | (event: ResizeObserverEntry) => void |
undefined |
Element resizing callback. |
Virtual List Methods
| Name | Type | Description |
|---|---|---|
| scrollTo | ScrollTo |
Scroll to a position. |
ScrollTo Type
interface ScrollTo {
(x: number, y: number): void
(options: {
left?: number
top?: number
behavior?: ScrollBehavior
debounce?: boolean
}): void
(options: {
index: number
behavior?: ScrollBehavior
debounce?: boolean
}): void
(options: {
key: string | number
behavior?: ScrollBehavior
debounce?: boolean
}): void
(options: {
position: 'top' | 'bottom'
behavior?: ScrollBehavior
debounce?: boolean
}): void
}
Basic Usage
Fixed Size Items
<template>
<n-virtual-list style="max-height: 240px" :item-size="42" :items="items">
<template #default="{ item }">
<div :key="item.key" class="item" style="height: 42px">
<img class="avatar" :src="item.avatar" alt="">
<span>{{ item.value }}</span>
</div>
</template>
</n-virtual-list>
</template>
<script setup>
import { ref } from 'vue'
const items = ref(
Array.from({ length: 1000 }).map((_, i) => ({
key: i,
value: `Item ${i}`,
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${i}`
}))
)
</script>
<style scoped>
.item {
display: flex;
align-items: center;
}
.avatar {
width: 28px;
border-radius: 50%;
margin-right: 10px;
}
</style>
Dynamic Size Items
<template>
<n-virtual-list
style="max-height: 240px"
:item-size="42"
:items="items"
item-resizable
>
<template #default="{ item, index }">
<div :key="item.key" class="item">
<img class="avatar" :src="item.avatar" alt="">
<span>{{ index }} - {{ item.message }}</span>
</div>
</template>
</n-virtual-list>
</template>
Common Patterns
Scroll to Position
<template>
<n-space vertical>
<n-space>
<n-button @click="handleScrollToKey">
Scroll to Key
</n-button>
<n-button @click="handleScrollToPosition">
Scroll to Position
</n-button>
<n-button @click="handleScrollToIndex">
Scroll to Index
</n-button>
<n-button @click="handleScrollToDistance">
Scroll to Distance
</n-button>
</n-space>
<n-virtual-list
ref="virtualListInst"
style="max-height: 240px"
:item-size="42"
:items="items"
item-resizable
>
<template #default="{ item, index }">
<div :key="item.key" class="item" style="height: 42px">
<img class="avatar" :src="item.avatar" alt="">
<span>{{ index }}</span>
</div>
</template>
</n-virtual-list>
</n-space>
</template>
<script setup>
import { ref } from 'vue'
const virtualListInst = ref(null)
const items = ref([])
const handleScrollToKey = () => {
virtualListInst.value?.scrollTo({ key: 500, behavior: 'smooth' })
}
const handleScrollToPosition = () => {
virtualListInst.value?.scrollTo({ top: 1000, behavior: 'smooth' })
}
const handleScrollToIndex = () => {
virtualListInst.value?.scrollTo({ index: 200, behavior: 'smooth' })
}
const handleScrollToDistance = () => {
virtualListInst.value?.scrollTo({ position: 'bottom', behavior: 'smooth' })
}
</script>
Keep State with Keep-alive
<template>
<n-space vertical>
<n-button @click="showVirtualList = !showVirtualList">
Toggle visibility
</n-button>
<keep-alive>
<n-virtual-list
v-if="showVirtualList"
style="max-height: 240px"
:item-size="42"
:items="items"
item-resizable
>
<template #default="{ item, index }">
<div :key="item.key" class="item" style="height: 42px">
<img class="avatar" :src="item.avatar" alt="">
<span>{{ index }}</span>
</div>
</template>
</n-virtual-list>
</keep-alive>
</n-space>
</template>
<script setup>
import { ref } from 'vue'
const showVirtualList = ref(true)
const items = ref([])
</script>
With Custom Scrollbar
<template>
<n-virtual-list
style="max-height: 240px"
:item-size="42"
:items="items"
:scrollbar-props="{ trigger: 'none', size: 10 }"
>
<template #default="{ item, index }">
<div :key="item.key" style="height: 42px; padding: 8px">
{{ index }}: {{ item.value }}
</div>
</template>
</n-virtual-list>
</template>
Chat Message List
<template>
<n-virtual-list
ref="chatListRef"
style="height: 400px"
:item-size="60"
:items="messages"
item-resizable
:default-scroll-index="messages.length - 1"
>
<template #default="{ item }">
<div :key="item.id" class="message">
<div class="sender">{{ item.sender }}</div>
<div class="content">{{ item.content }}</div>
</div>
</template>
</n-virtual-list>
</template>
<script setup>
import { ref } from 'vue'
const messages = ref([
{ id: 1, sender: 'Alice', content: 'Hello!' },
{ id: 2, sender: 'Bob', content: 'Hi there!' }
])
</script>
Best Practices
Set item-size: Always provide a reasonable
item-sizeestimate for better scroll calculationUse item-resizable: Enable
item-resizablewhen items have variable heightsUnique keys: Ensure each item has a unique key field
Performance: Use
ignore-item-resizeif items don't resize for slightly better performanceKeep-alive: Use keep-alive to preserve scroll state when toggling visibility
Debounce scroll: Use
debounce: truein scrollTo for smoother scrollingScrollbar props: Customize scrollbar via
scrollbar-props