NbTree and NbTreeNode provide a hierarchical tree view for navigating nested data structures like file systems, project explorers, or organizational charts.
Basic Usage
<template>
<NbTree v-model="selected">
<NbTreeNode id="1" label="Documents" icon="folder">
<NbTreeNode id="1.1" label="Resume.pdf" icon="file" />
<NbTreeNode id="1.2" label="Cover Letter.docx" icon="file" />
</NbTreeNode>
<NbTreeNode id="2" label="Images" icon="folder">
<NbTreeNode id="2.1" label="photo.png" icon="image" />
</NbTreeNode>
<NbTreeNode id="3" label="README.md" icon="file" />
</NbTree>
</template>
<script setup>
import { ref } from 'vue'
const selected = ref(null)
</script>Branch vs Leaf Nodes
Nodes with child NbTreeNode elements are branches -- they show a caret chevron and can be expanded/collapsed. Nodes without children are leaves.
Compact Mode
Use the compact prop or size="sm" for denser tree views (24px row height instead of 32px).
Drag and Drop
Enable draggable on the tree to allow nodes to be reordered via drag and drop. The tree emits a drop event with the source node, target node, and drop position (before, after, or inside).
<template>
<NbTree v-model="selected" draggable @drop="onDrop">
<NbTreeNode id="1" label="Documents" icon="folder">
<NbTreeNode id="1.1" label="Resume.pdf" icon="file" />
<NbTreeNode id="1.2" label="Cover Letter.docx" icon="file" />
</NbTreeNode>
<NbTreeNode id="2" label="Images" icon="folder">
<NbTreeNode id="2.1" label="photo.png" icon="image" />
</NbTreeNode>
<NbTreeNode id="3" label="README.md" icon="file" />
</NbTree>
</template>
<script setup>
import { ref } from 'vue'
const selected = ref(null)
function onDrop({ sourceId, targetId, position }) {
console.log(`Move ${sourceId} ${position} ${targetId}`)
// Reorder your data model here
}
</script>Visual indicators during drag:
- Before/After: A colored line appears at the top or bottom edge of the target row.
- Inside (any node that has not opted out with
droppable): The target row highlights with a tinted background and border, indicating the dragged node will become a child. - The dragged node fades to 40% opacity while in flight.
Individual nodes can opt out of dragging by setting :draggable="false", or you can enable it per-node instead of globally.
Dropping onto a leaf
Every node takes a drop in its middle by default, including a node with no children, which is how a leaf becomes a parent. Set :droppable="false" on a node that should never take children:
<NbTree draggable @drop="onDrop">
<NbTreeNode
v-for="page in pages"
:id="page.id"
:key="page.id"
:label="page.title"
>
<NbTreeNode
v-for="child in page.children"
:id="child.id"
:key="child.id"
:label="child.title"
/>
</NbTreeNode>
<NbTreeNode id="readme" label="Cannot take children" :droppable="false" />
</NbTree>A node is a branch when its default slot renders at least one child, checked on every render. A slot that renders nothing (an empty v-for, as for page above before it has children) leaves a plain leaf: no caret, no toggle on click, and no aria-expanded. When the first child arrives the caret appears without the node remounting. For children loaded only on expand, set expandable to show the caret before they exist.
A node can never go inside itself
A node dropped into its own subtree is detached from the root, and in a stored tree everything under it disappears. The tree refuses it: while a node is being dragged, itself and every row under it show no drop indicator and fire no drop event, so the pointer keeps the browser's "cannot drop" cursor. Nothing is needed from the product.
Working out the move
A drop event says what was dropped where. Turning that into a new tree, and offering the same move from a keyboard "Move to" picker, is the same work in every product, so the library ships it as plain functions. They take any tree of { id, children }, never change the one they are given, and enforce the same subtree rule the drag does.
import { planTreeMove, moveTargets, isInvalidTarget } from '@nubisco/ui'
function onDrop(event) {
const plan = planTreeMove(pages.value, event.sourceId, {
kind: event.position, // 'before' | 'after' | 'inside'
target: event.targetId,
})
// null when the move is refused or would change nothing.
if (!plan) return
const previous = pages.value
pages.value = plan.tree // optimistic
api.move(event.sourceId, plan).catch(() => (pages.value = previous))
}| Function | Answers |
|---|---|
planTreeMove(tree, id, placement) | The tree after the move, plus parentId, index, afterId and beforeId |
moveTargets(tree, id) | Every node it may move to, in reading order with depth, for a picker |
isInvalidTarget(tree, source, id) | Whether a target is the node itself or inside it |
ancestorsOf(tree, id) | The ids above a node, outermost first, to expand its parents |
subtreeOf(tree, id) | A node and everything under it |
nestByDepth(rows) | A flat, depth-ordered API response rebuilt into a tree |
planTreeMove returns null when the move is refused or would leave the tree as it is, so a product never sends a pointless write. placement is { kind: 'inside' | 'before' | 'after', target } or { kind: 'root' } for the top level. "Inside" appends as the last child.
Actions Slot
Use the actions slot on NbTreeNode to render action buttons or badges on the right side. Actions are visible on hover and when the node is selected.
Custom Label Slot
Use the label slot to render custom content (e.g., inline rename inputs).
<NbTreeNode id="1" label="Editable">
<template #label>
<input v-if="renaming" v-model="name" @blur="save" />
<span v-else>{{ name }}</span>
</template>
</NbTreeNode>Deep Nesting
Trees support unlimited nesting depth. Indentation increases by 16px per level.
Programmatic Control
Use ref to access expandIds() and collapseAll() methods.
<template>
<NbButton @click="treeRef.collapseAll()">Collapse All</NbButton>
<NbTree ref="treeRef">...</NbTree>
</template>
<script setup>
const treeRef = ref()
</script>