Tree
A hierarchical tree view component for displaying nested data with expand/collapse, selection, checkboxes, drag & drop, filtering, lazy loading, inline editing, and more.
Basic
Click on a branch node's arrow to expand or collapse it.
<script setup>
import { VxTree } from 'veloxui'
const data = [
{
key: '1', label: 'Documents',
children: [
{ key: '1-1', label: 'Work',
children: [
{ key: '1-1-1', label: 'Reports.xlsx' },
{ key: '1-1-2', label: 'Presentation.pptx' },
],
},
{ key: '1-2', label: 'Personal',
children: [
{ key: '1-2-1', label: 'Resume.pdf' },
],
},
],
},
]
</script>
<template>
<VxTree :data="data" />
</template>Selection
Use v-model:selected to bind the selected node key. Enabled by default.
Selected: none
<script setup>
import { ref } from 'vue'
const selectedKey = ref(null)
</script>
<template>
<VxTree :data="data" v-model:selected="selectedKey" />
</template>Multiple Selection
Enable multiple to select more than one node. Hold Ctrl (or Cmd on Mac) and click to toggle individual nodes.
Selected: none
<script setup>
import { ref } from 'vue'
const selected = ref([])
</script>
<template>
<VxTree :data="data" v-model:selected="selected" multiple default-expand-all />
</template>Checkboxes
Add checkboxes with checkable. Parent nodes show indeterminate state when partially checked.
Checked: none
<script setup>
import { ref } from 'vue'
const checkedKeys = ref([])
</script>
<template>
<VxTree :data="data" checkable v-model:checked="checkedKeys" default-expand-all />
</template>Show Lines
Display connecting lines with show-line. Use line-style to choose between solid, dashed, or dotted.
<template>
<VxTree :data="data" show-line default-expand-all />
<VxTree :data="data" show-line line-style="dashed" default-expand-all />
<VxTree :data="data" show-line line-style="dotted" default-expand-all />
</template>Filter with Highlight
Use filter-text to filter visible nodes. Matching text is highlighted automatically. The tree auto-expands to show matching children and restores previous state when the filter is cleared.
<script setup>
import { ref } from 'vue'
const filterText = ref('')
</script>
<template>
<VxInput v-model="filterText" placeholder="Type to filter..." />
<VxTree :data="data" :filter-text="filterText" default-expand-all />
</template>Default Expand All
Use default-expand-all to expand every branch on mount.
Accordion
Only one node per level can be expanded at a time. Expanding a node collapses its siblings.
<template>
<VxTree :data="data" accordion />
</template>Node Icons
Use the icon property on node data, or customize with the #icon slot.
<script setup>
const data = [
{
key: 'src', label: 'src', icon: '📁',
children: [
{ key: 'index', label: 'index.ts', icon: '📄' },
{ key: 'styles', label: 'styles.css', icon: '🎨' },
],
},
{ key: 'pkg', label: 'package.json', icon: '📦' },
]
</script>
<template>
<VxTree :data="data" default-expand-all />
</template>Use the #icon slot for full control:
<template>
<VxTree :data="data">
<template #icon="{ node, expanded }">
<MyIcon :name="node.children ? (expanded ? 'folder-open' : 'folder') : 'file'" />
</template>
</VxTree>
</template>Children Count
Show a badge with the number of children using show-count. Customize with the #count slot.
<template>
<VxTree :data="data" show-count default-expand-all />
</template>Node Actions
Use the #actions slot to add hover-visible action buttons to each node.
<template>
<VxTree :data="data" default-expand-all>
<template #actions="{ node }">
<button @click="editNode(node)">Edit</button>
<button @click="deleteNode(node)">Delete</button>
</template>
</VxTree>
</template>Drag & Drop
Enable draggable to allow reordering nodes. The node-drop event provides dragNode, dropNode, and position (before, inside, or after) for you to handle the data update.
<script setup>
import { ref } from 'vue'
const data = ref([...])
function onDrop(info) {
console.log(info.dragNode.label, info.position, info.dropNode.label)
// Handle data mutation here
}
</script>
<template>
<VxTree :data="data" draggable @node-drop="onDrop" />
</template>Customize the drag handle with the #drag-handle slot:
<template>
<VxTree :data="data" draggable>
<template #drag-handle="{ node }">
<MyGripIcon />
</template>
</VxTree>
</template>Lazy Loading
Pass a load function to fetch children on demand. Just provide your data and a loader — the tree handles the rest:
- A spinner shows while loading
- Empty results auto-mark the node as a leaf (no more load attempts)
- Errors show inline with a Retry button
- Already-loaded nodes are cached — expanding again won't re-fetch
- Use
isLeaf: trueon nodes you know are leaves to skip the load entirely
Basic
Click the arrows to load children on demand. "Empty Folder" returns no results and becomes a leaf.
<script setup>
import { ref } from 'vue'
const data = ref([
{ key: 'folder-a', label: 'Folder A' },
{ key: 'folder-b', label: 'Folder B' },
{ key: 'empty', label: 'Empty Folder' },
{ key: 'readme', label: 'readme.txt', isLeaf: true },
])
async function load(node) {
const res = await fetch(`/api/${node.key}/children`)
return await res.json() // return TreeNodeData[]
}
</script>
<template>
<VxTree :data="data" :load="load" />
</template>Error Handling & Retry
If load throws, the tree shows the error message with a retry button. Click "Orders" below to see the error state.
<script setup>
async function load(node) {
const res = await fetch(`/api/${node.key}/children`)
if (!res.ok) throw new Error('Failed to fetch')
return await res.json()
}
</script>
<template>
<!-- Errors show inline with a Retry button -->
<VxTree :data="data" :load="load" />
</template>Editable Labels
Enable editable to allow inline renaming. Double-click a label to edit it. Press Enter to save, Escape to cancel. Handle the node-edit event to apply changes.
Double-click a label to rename it
<script setup>
function onEdit({ node, newLabel, oldLabel }) {
node.label = newLabel // apply the change
}
</script>
<template>
<VxTree :data="data" editable @node-edit="onEdit" />
</template>Context Menu
Use the #context-menu slot to add a right-click menu. The slot provides node and close.
Right-click any node to see the context menu
<template>
<VxTree :data="data">
<template #context-menu="{ node, close }">
<div class="menu">
<button @click="openNode(node); close()">Open</button>
<button @click="renameNode(node); close()">Rename</button>
<button @click="deleteNode(node); close()">Delete</button>
</div>
</template>
</VxTree>
</template>Striped Rows
Add striped for alternating row backgrounds to improve readability.
<template>
<VxTree :data="data" striped default-expand-all />
</template>Virtual Scrolling
For large datasets, enable virtual to only render visible nodes. Set virtual-height for the container height and item-height for each row.
50 categories × 20 items = 1,000 nodes
<script setup>
// Generate 1,000 nodes
const data = Array.from({ length: 50 }, (_, i) => ({
key: `cat-${i}`,
label: `Category ${i + 1}`,
children: Array.from({ length: 20 }, (_, j) => ({
key: `cat-${i}-item-${j}`,
label: `Item ${i + 1}-${j + 1}`,
isLeaf: true,
})),
}))
</script>
<template>
<VxTree :data="data" virtual :virtual-height="300" :item-height="30" default-expand-all />
</template>Disabled Nodes
Nodes with disabled: true cannot be selected or checked.
Custom Content
Use the default slot for fully custom node rendering. Receives node, expanded, selected, and checkState.
<template>
<VxTree :data="data" default-expand-all>
<template #default="{ node, expanded }">
<span>{{ node.children ? (expanded ? '📂' : '📁') : '📄' }} {{ node.label }}</span>
</template>
</VxTree>
</template>API
VxTree Props
| Prop | Type | Default | Description |
|---|---|---|---|
data | TreeNodeData[] | [] | Tree data |
selected | string | number | null | array | null | Selected key(s) (v-model) |
checked | (string | number)[] | [] | Checked keys (v-model) |
expanded | (string | number)[] | — | Expanded keys (v-model) |
default-expand-all | boolean | false | Expand all on mount |
default-expanded-keys | (string | number)[] | [] | Keys to expand on mount |
checkable | boolean | false | Show checkboxes |
selectable | boolean | true | Enable selection |
multiple | boolean | false | Allow multi-selection (Ctrl/Cmd+Click) |
show-line | boolean | false | Show connecting lines |
line-style | 'solid' | 'dashed' | 'dotted' | 'solid' | Line style |
accordion | boolean | false | One node per level |
indent | number | 24 | Indent per level (px) |
filter-text | string | '' | Filter by label |
highlight-filter | boolean | true | Highlight matched filter text |
draggable | boolean | false | Enable drag & drop |
editable | boolean | false | Enable double-click rename |
show-count | boolean | false | Show children count badge |
striped | boolean | false | Alternating row backgrounds |
load | (node) => Promise<TreeNodeData[]> | — | Async children loader |
virtual | boolean | false | Enable virtual scrolling |
virtual-height | number | 400 | Virtual container height (px) |
item-height | number | 30 | Virtual item height (px) |
TreeNodeData
| Property | Type | Required | Description |
|---|---|---|---|
key | string | number | Yes | Unique identifier |
label | string | Yes | Display text |
children | TreeNodeData[] | No | Child nodes |
disabled | boolean | No | Disable interaction |
isLeaf | boolean | No | Force leaf (skip lazy load) |
icon | string | No | Node icon (emoji or text) |
Events
| Event | Payload | Description |
|---|---|---|
update:selected | key | keys[] | Selection changed |
update:checked | keys[] | Checked state changed |
update:expanded | keys[] | Expanded state changed |
node-click | node | Node clicked |
node-expand | node | Node expanded |
node-collapse | node | Node collapsed |
node-drop | { dragNode, dropNode, position } | Node dropped (drag & drop) |
node-drag-start | node | Drag started |
node-edit | { node, newLabel, oldLabel } | Label edited |
node-context-menu | { node, event } | Right-click on node |
node-loaded | node, children | Lazy load completed |
Slots
| Slot | Props | Description |
|---|---|---|
default | { node, expanded, selected, checkState } | Custom node content |
icon | { node, expanded } | Custom node icon |
actions | { node } | Hover action buttons |
drag-handle | { node } | Custom drag handle |
count | { node, count } | Custom count badge |
context-menu | { node, close } | Right-click context menu |
empty | — | Content when data is empty |