createNested
Hierarchical tree management built on createGroup, with parent-child relationships, open/close state, and pluggable traversal strategies.
Usage
The createNested composable manages hierarchical tree structures with parent-child relationships, open/close states, and tree traversal.
import { createNested } from '@vuetify/v0'
const tree = createNested({ open: 'multiple', selection: 'cascade' })
tree.onboard([
{
id: 'root',
value: 'Root',
children: [
{ id: 'child-1', value: 'Child 1' },
{ id: 'child-2', value: 'Child 2' },
],
},
])
tree.open('root')
tree.select('child-1')Context / DI
Context Pattern
Use with Vue’s provide/inject for component trees:
import { createNestedContext } from '@vuetify/v0'
// Create a trinity
const [useTree, provideTree, defaultTree] = createNestedContext({
namespace: 'my-tree',
})
// In parent component
provideTree()
// In child components
const tree = useTree()Architecture
createNested extends createGroup with hierarchical tree management:
Options
open
Controls how nodes expand/collapse:
| Value | Behavior |
|---|---|
'multiple' | Multiple nodes can be open simultaneously (default) |
'single' | Only one node open at a time (accordion behavior) |
// Tree view - multiple nodes open
const tree = createNested({ open: 'multiple' })
// Accordion - single node open
const accordion = createNested({ open: 'single' })mandatory
When true, deselecting is prevented if it would leave no items selected:
const tree = createNested({ selection: 'cascade', mandatory: true })
tree.select('child-1')
tree.unselect('child-1') // no-op — would deselect the only selected itemunselectAll() with mandatory: true keeps the first selected item rather than clearing.
multiple
When false, selecting a node in cascade mode clears previous selections first (default: true):
const tree = createNested({ selection: 'cascade', multiple: false })
tree.select('child-1')
tree.select('child-2') // child-1 is deselected firstdisabled
When true, all tree mutations (open(), close(), select(), unselect(), toggle()) become no-ops. Individual tickets can also carry a disabled flag to skip only that node:
const tree = createNested({ disabled: true })
tree.open('branch-1') // no-op — tree is disabled
tree.select('leaf-1') // no-opAccepts MaybeRefOrGetter<boolean> for reactive toggling:
const isLocked = shallowRef(false)
const tree = createNested({ disabled: isLocked })selection
Controls how selection cascades through the hierarchy:
| Value | Behavior |
|---|---|
'cascade' | Selecting parent selects all descendants; ancestors show mixed state (default) |
'independent' | Each node selected independently, no cascading |
'leaf' | Only leaf nodes can be selected; parent selection selects leaf descendants |
// Cascading checkbox tree
const tree = createNested({ selection: 'cascade' })
// Independent selection
const flat = createNested({ selection: 'independent' })
// Leaf-only selection (file picker)
const picker = createNested({ selection: 'leaf' })Reactivity
createNested uses shallowReactive for tree state, making structural changes reactive while keeping traversal methods non-reactive for performance.
| Property/Method | Reactive | Notes |
|---|---|---|
children | ShallowReactive Map | |
parents | ShallowReactive Map | |
openedIds | ShallowReactive Set | |
openedItems | Computed from openedIds | |
rootIds | ShallowReactive Set — IDs of all top-level (parentless) nodes | |
roots | Computed, root nodes | |
leaves | Computed, leaf nodes | |
ticket.isOpen | Ref via toRef() | |
ticket.isLeaf | Ref via toRef() | |
ticket.depth | Ref via toRef() |
Ticket Properties
Each registered node receives additional properties:
const node = tree.register({ id: 'node', value: 'Node', parentId: 'root' })
// Reactive refs
node.isOpen.value // boolean - is this node open?
node.isLeaf.value // boolean - has no children?
node.depth.value // number - depth in tree (0 = root)
// Methods
node.open() // Open this node
node.close() // Close this node
node.flip() // Flip open/closed state
node.getPath() // Get path from root to this node
node.getAncestors() // Get all ancestors
node.getDescendants() // Get all descendantsExamples
Recipes
Selection Modes
Cascade Mode (Default)
Selection propagates through the hierarchy:
Selecting a parent selects all descendants:
tree.select('root')
// root, child-1, child-2, grandchild-1, etc. are all selectedSelecting a child updates ancestors to mixed state:
tree.select('child-1')
// child-1 is selected
// root shows mixed state (some children selected)Automatic state resolution:
All children selected → Parent becomes selected (not mixed)
Some children selected → Parent becomes mixed
No children selected → Parent becomes unselected (not mixed)
Independent Mode
Each node is selected independently with no cascading:
const tree = createNested({ selection: 'independent' })
tree.select('parent')
// Only 'parent' is selected, children unchangedLeaf Mode
Only leaf nodes can be selected. Selecting a parent selects all leaf descendants:
const tree = createNested({ selection: 'leaf' })
tree.select('folder')
// All files (leaves) under 'folder' are selected
// 'folder' itself is not in selectedIdsConvenience Methods
Expand/Collapse All
// Open all non-leaf nodes
tree.expandAll()
// Close all nodes
tree.collapseAll()Data Transformation
Convert tree to flat array for serialization or API consumption:
const flat = tree.toFlat()
// Returns: [{ id, parentId, value }, ...]
// Useful for sending to APIs or AI systems
console.log(JSON.stringify(flat))Inline Children Registration
Define children directly when registering items:
tree.onboard([
{
id: 'nav',
value: 'Navigation',
children: [
{ id: 'home', value: 'Home' },
{ id: 'about', value: 'About' },
{
id: 'products',
value: 'Products',
children: [
{ id: 'widgets', value: 'Widgets' },
{ id: 'gadgets', value: 'Gadgets' },
],
},
],
},
])Cascade Unregister
Remove a node and optionally all its descendants:
// Remove node, orphan children (default)
tree.unregister('parent')
// Remove node and all descendants
tree.unregister('parent', true)
// Batch removal with cascade
tree.offboard(['node-1', 'node-2'], true)FAQ
cascade (default) selects all descendants when you select a parent and shows ancestors as mixed; independent selects each node alone with no propagation; leaf lets only leaf nodes be selected, so selecting a parent selects its leaf descendants — the file-picker pattern.
Pass open: 'single' so only one node can be open at a time. The default open: 'multiple' lets several branches stay expanded simultaneously.
Use createNested when you’re building custom tree UI and only want the hierarchy logic. The Treeview component wraps it with focus management and ARIA already wired up. For flat multi-select with no hierarchy, createGroup is lighter.
By default they’re orphaned — unregister('parent') removes only that node. Pass true to cascade: unregister('parent', true) removes the node and every descendant, and offboard(['a', 'b'], true) does the same for a batch.
Each registered ticket exposes getPath() (root-to-node), getAncestors(), and getDescendants(). Use them to highlight a node’s lineage or operate on a whole subtree.
Call toFlat() — it returns a flat [{ id, parentId, value }, ...] array (one entry per node) that you can JSON.stringify and send to an API or persist.
Benchmarks
Every operation is profiled across multiple dataset sizes to measure real-world throughput. Each benchmark is assigned a performance tier—good, fast, blazing, or slow—and groups are scored by averaging their individual results so you can spot bottlenecks at a glance. This transparency helps you make informed decisions about which patterns scale for your use case. Learn more in the benchmarks guide.