VirtualTree
A tree view with expand/collapse, configurable indent per depth level, and optional lazy (async) child loading.
Props
nodes
TreeNode<T>[]
Root nodes.
indent
number · default: 20
Pixel indent per depth level.
estimatedItemSize
number · default: 36
Estimated row height.
overscan
number · default: 5
Extra rows outside the viewport.
onLoadChildren
(node) => Promise<TreeNode<T>[]>
Called when a node with hasChildren: true is first expanded.
motionBlur
boolean · default: false
Apply a CSS blur that scales with scroll velocity while scrolling fast.
Tree Node
TreeNode<T> — the shape of each entry in the nodes prop.
interface TreeNode<T extends Record<string, unknown> = Record<string, unknown>> {
id: string | number
data: T
children?: TreeNode<T>[]
hasChildren?: boolean // true = has children that haven't been loaded yet
}Row Data
FlatTreeRow<T> — the slot scope exposes:
interface FlatTreeRow<T> {
node: TreeNode<T>
depth: number
isExpanded: boolean
hasChildren: boolean
isLoading: boolean // true while onLoadChildren is running
}Slots
default
Scope: { row: FlatTreeRow<T>, index: number }
Custom row content. The toggle button is rendered by the component.
empty
Scope: No scope
Empty state.
Emits
node-expand
Payload: TreeNode<T>
node-collapse
Payload: TreeNode<T>
node-click
Payload: [TreeNode<T>, depth: number]
Exposed API
expandAll()
Expand every node, including nested children already loaded.
collapseAll()
Collapse every node.
scrollTo(index, align?, options?)
Scroll to a flat row index. align is 'start' | 'center' | 'end' | 'auto'.
getScrollElement()
Returns the element that actually scrolls — pair with VirtualScrollbar.
expandedIds
Readonly<Ref<Set<string | number>>> — the currently expanded node IDs.
treeRef.value?.expandAll()
treeRef.value?.collapseAll()
treeRef.value?.scrollTo(index, 'start')
treeRef.value?.scrollTo(index, 'start', { behavior: 'smooth' })
treeRef.value?.getScrollElement() // pair with VirtualScrollbar
treeRef.value?.expandedIds // Readonly<Ref<Set<string | number>>>Example
<script setup lang="ts">
import { VirtualTree } from 'vue-virtual-scroller-kit'
import type { TreeNode, FlatTreeRow } from 'vue-virtual-scroller-kit'
interface FileNode {
name: string
type: 'file' | 'folder'
}
const nodes: TreeNode<FileNode>[] = [
{
id: 1,
data: { name: 'src', type: 'folder' },
children: [
{ id: 2, data: { name: 'main.ts', type: 'file' } },
{ id: 3, data: { name: 'App.vue', type: 'file' } },
],
},
{
id: 4,
data: { name: 'node_modules', type: 'folder' },
hasChildren: true, // lazy-loaded
},
]
async function loadChildren(node: TreeNode<FileNode>): Promise<TreeNode<FileNode>[]> {
const res = await fetch(`/api/children/${node.id}`)
return res.json()
}
</script>
<template>
<VirtualTree
:nodes="nodes"
:indent="20"
:on-load-children="loadChildren"
style="height: 400px"
@node-click="(node, depth) => console.log(node.data.name, depth)"
>
<template #default="{ row }">
<span>{{ row.node.data.type === 'folder' ? '📁' : '📄' }} {{ row.node.data.name }}</span>
</template>
</VirtualTree>
</template>