Overview
The createNested composable extendscreateGroup to support parent-child relationships, tree traversal, and hierarchical state management. Perfect for tree views, nested navigation, file explorers, and organizational charts.
Signature
function createNested<
Z extends NestedTicketInput = NestedTicketInput,
E extends NestedTicket<Z> = NestedTicket<Z>
>(options?: NestedOptions): NestedContext<Z, E>
Configuration options
Show properties
Show properties
Controls how nodes expand/collapse:
'multiple': Multiple nodes can be open simultaneously'single': Only one node open at a time (accordion behavior)
Auto-open parent nodes when children are registered
When opening a node, also open all its ancestors (ensures visibility)
Controls how selection cascades:
'cascade': Selecting parent selects descendants; ancestors show mixed state'independent': Each node selected independently'leaf': Only leaf nodes selectable; selecting parent selects all leaf descendants
Controls active/highlight state:
'single': Only one item active at a time'multiple': Multiple items can be active
Mandatory selection enforcement (inherited from createGroup)
Allow multiple selections (inherited from createGroup)
Nested tree instance with hierarchical methods
Show properties
Show properties
Map of parent IDs to arrays of child IDs
Map of child IDs to parent ID (undefined for roots)
Array of root items (items with no parent)
Array of leaf items (items with no children)
Set of opened/expanded item IDs
Set of opened/expanded item instances
Set of active/highlighted item IDs
Set of active/highlighted item instances
Set of active item indexes
Open/expand one or more nodes
Close/collapse one or more nodes
Toggle open/closed state of one or more nodes
Check if a node is open
Open node(s) and their immediate non-leaf children
Reveal node(s) by opening all ancestors (makes visible without opening node itself)
Fully expand node(s) and all their non-leaf descendants
Expand all non-leaf nodes in the tree
Collapse all nodes
Activate/highlight one or more nodes
Deactivate/unhighlight one or more nodes
Check if a node is active
Deactivate all nodes
Get path from root to item (includes item)
Get all ancestors (excludes item)
Get all descendants
Check if item is a leaf node
Get depth level (0 = root)
Check if ancestorId is an ancestor of descendantId
Check if id has ancestorId as an ancestor
Get sibling IDs (including self)
Get 1-indexed position among siblings (for aria-posinset)
Convert tree to flat array with parentId references
Select item(s) with cascading based on selection mode
Unselect item(s) with cascading based on selection mode
Toggle selection with cascading
Register a node with optional parentId and inline children
Unregister node. If
cascade: true, removes all descendants; otherwise orphans them.Usage
Basic Tree Structure
import { createNested } from '@vuetify/v0'
const tree = createNested()
const root = tree.register({ id: 'root', value: 'Root Folder' })
const child1 = tree.register({ id: 'child-1', value: 'Documents', parentId: 'root' })
const child2 = tree.register({ id: 'child-2', value: 'Photos', parentId: 'root' })
const grandchild = tree.register({ id: 'gc-1', value: 'Vacation', parentId: 'child-2' })
console.log(tree.getPath('gc-1')) // ['root', 'child-2', 'gc-1']
console.log(tree.getDescendants('root')) // ['child-1', 'child-2', 'gc-1']
console.log(tree.isLeaf('gc-1')) // true
Inline Children
const tree = createNested()
tree.register({
id: 'root',
value: 'Root',
children: [
{
id: 'child-1',
value: 'Child 1',
children: [
{ id: 'grandchild-1', value: 'Grandchild 1' }
]
},
{ id: 'child-2', value: 'Child 2' }
]
})
console.log(tree.size) // 4
console.log(tree.getPath('grandchild-1')) // ['root', 'child-1', 'grandchild-1']
Cascading Selection (Default)
const tree = createNested({ selection: 'cascade' })
tree.register({ id: 'root', value: 'Root' })
tree.register({ id: 'child-1', value: 'Child 1', parentId: 'root' })
tree.register({ id: 'child-2', value: 'Child 2', parentId: 'root' })
// Select parent selects all descendants
tree.select('root')
console.log(tree.selected('root')) // true
console.log(tree.selected('child-1')) // true
console.log(tree.selected('child-2')) // true
// Unselect all
tree.unselect('root')
// Select one child - parent becomes mixed
tree.select('child-1')
console.log(tree.selected('child-1')) // true
console.log(tree.mixed('root')) // true (indeterminate)
// Select all children - parent becomes selected
tree.select('child-2')
console.log(tree.selected('root')) // true
console.log(tree.mixed('root')) // false
Independent Selection
const tree = createNested({ selection: 'independent' })
tree.register({ id: 'root', value: 'Root' })
tree.register({ id: 'child', value: 'Child', parentId: 'root' })
// Selection doesn't cascade
tree.select('root')
console.log(tree.selected('root')) // true
console.log(tree.selected('child')) // false
console.log(tree.mixed('root')) // false
Leaf-Only Selection
const tree = createNested({ selection: 'leaf' })
tree.register({ id: 'root', value: 'Root' })
tree.register({ id: 'branch', value: 'Branch', parentId: 'root' })
tree.register({ id: 'leaf-1', value: 'Leaf 1', parentId: 'branch' })
tree.register({ id: 'leaf-2', value: 'Leaf 2', parentId: 'branch' })
// Selecting parent only selects leaf descendants
tree.select('root')
console.log(tree.selected('root')) // false (not a leaf)
console.log(tree.selected('branch')) // false (not a leaf)
console.log(tree.selected('leaf-1')) // true
console.log(tree.selected('leaf-2')) // true
Open/Close State
const tree = createNested()
tree.register({ id: 'folder', value: 'My Folder' })
tree.register({ id: 'file', value: 'document.txt', parentId: 'folder' })
// Open folder to show contents
tree.open('folder')
console.log(tree.opened('folder')) // true
// Close folder to hide contents
tree.close('folder')
console.log(tree.opened('folder')) // false
// Toggle
tree.flip('folder')
console.log(tree.opened('folder')) // true
Single Open Mode (Accordion)
const accordion = createNested({ open: 'single' })
accordion.onboard([
{ id: 'panel-1', value: 'Panel 1' },
{ id: 'panel-2', value: 'Panel 2' },
{ id: 'panel-3', value: 'Panel 3' },
])
accordion.open('panel-1')
console.log(accordion.opened('panel-1')) // true
// Opening another closes the first
accordion.open('panel-2')
console.log(accordion.opened('panel-1')) // false
console.log(accordion.opened('panel-2')) // true
Active State
const tree = createNested({ active: 'single' })
tree.onboard([
{ id: 'node-1', value: 'Node 1' },
{ id: 'node-2', value: 'Node 2' },
])
tree.activate('node-1')
console.log(tree.activated('node-1')) // true
// Single mode clears previous active
tree.activate('node-2')
console.log(tree.activated('node-1')) // false
console.log(tree.activated('node-2')) // true
Tree Traversal
const tree = createNested()
tree.register({ id: 'root', value: 'Root' })
tree.register({ id: 'a', value: 'A', parentId: 'root' })
tree.register({ id: 'b', value: 'B', parentId: 'root' })
tree.register({ id: 'a1', value: 'A1', parentId: 'a' })
tree.register({ id: 'a2', value: 'A2', parentId: 'a' })
// Get full path
console.log(tree.getPath('a1')) // ['root', 'a', 'a1']
// Get ancestors
console.log(tree.getAncestors('a1')) // ['root', 'a']
// Get descendants
console.log(tree.getDescendants('root')) // ['a', 'b', 'a1', 'a2']
console.log(tree.getDescendants('a')) // ['a1', 'a2']
// Check depth
console.log(tree.getDepth('root')) // 0
console.log(tree.getDepth('a')) // 1
console.log(tree.getDepth('a1')) // 2
// Check leaf status
console.log(tree.isLeaf('a1')) // true
console.log(tree.isLeaf('a')) // false
Ancestor Relationships
const tree = createNested()
tree.register({ id: 'root', value: 'Root' })
tree.register({ id: 'child', value: 'Child', parentId: 'root' })
tree.register({ id: 'grandchild', value: 'Grandchild', parentId: 'child' })
console.log(tree.isAncestorOf('root', 'grandchild')) // true
console.log(tree.isAncestorOf('child', 'grandchild')) // true
console.log(tree.isAncestorOf('grandchild', 'root')) // false
console.log(tree.hasAncestor('grandchild', 'root')) // true
Siblings and Position
const tree = createNested()
tree.register({ id: 'parent', value: 'Parent' })
tree.register({ id: 'child-1', value: 'First', parentId: 'parent' })
tree.register({ id: 'child-2', value: 'Second', parentId: 'parent' })
tree.register({ id: 'child-3', value: 'Third', parentId: 'parent' })
console.log(tree.siblings('child-2')) // ['child-1', 'child-2', 'child-3']
console.log(tree.position('child-2')) // 2 (1-indexed for aria-posinset)
Reveal and Expand
const tree = createNested()
tree.register({ id: 'root', value: 'Root' })
tree.register({ id: 'folder-1', value: 'Folder 1', parentId: 'root' })
tree.register({ id: 'folder-2', value: 'Folder 2', parentId: 'folder-1' })
tree.register({ id: 'file', value: 'file.txt', parentId: 'folder-2' })
// Reveal file by opening ancestors
tree.reveal('file')
console.log(tree.opened('root')) // true
console.log(tree.opened('folder-1')) // true
console.log(tree.opened('folder-2')) // true
console.log(tree.opened('file')) // false (reveal doesn't open the item itself)
// Expand folder-1 and all non-leaf descendants
tree.collapseAll()
tree.expand('folder-1')
console.log(tree.opened('folder-1')) // true
console.log(tree.opened('folder-2')) // true (non-leaf descendant)
console.log(tree.opened('file')) // false (leaf)
Unregister with Cascade
const tree = createNested()
tree.register({ id: 'root', value: 'Root' })
tree.register({ id: 'child', value: 'Child', parentId: 'root' })
tree.register({ id: 'grandchild', value: 'Grandchild', parentId: 'child' })
// Orphan children (default)
tree.unregister('root')
console.log(tree.has('root')) // false
console.log(tree.has('child')) // true (orphaned)
console.log(tree.parents.get('child')) // undefined
// Cascade delete
tree.register({ id: 'root2', value: 'Root 2' })
tree.register({ id: 'child2', value: 'Child 2', parentId: 'root2' })
tree.unregister('root2', true) // cascade = true
console.log(tree.has('root2')) // false
console.log(tree.has('child2')) // false (deleted)
Tree Component Example
<script setup lang="ts">
import { createNested } from '@vuetify/v0'
interface TreeNode extends NestedTicketInput {
label: string
icon?: string
}
const tree = createNested<TreeNode>({
selection: 'cascade',
open: 'multiple'
})
tree.onboard([
{
id: 'documents',
label: 'Documents',
icon: 'mdi-folder',
children: [
{ id: 'work', label: 'Work', icon: 'mdi-briefcase' },
{ id: 'personal', label: 'Personal', icon: 'mdi-account' },
]
},
{
id: 'downloads',
label: 'Downloads',
icon: 'mdi-download'
}
])
</script>
<template>
<div class="tree-view">
<TreeNode
v-for="root in tree.roots.value"
:key="root.id"
:node="root"
:tree="tree"
/>
</div>
</template>
<!-- TreeNode.vue (recursive component) -->
<template>
<div class="tree-node">
<div
class="node-content"
:class="{
selected: node.isSelected.value,
active: node.isActive.value
}"
>
<button
v-if="!node.isLeaf.value"
@click="node.flip()"
>
{{ node.isOpen.value ? '▼' : '▶' }}
</button>
<input
v-if="!node.isLeaf.value"
type="checkbox"
:checked="node.isSelected.value"
:indeterminate="node.isMixed.value"
@change="node.toggle()"
>
<span @click="node.activate()">{{ node.label }}</span>
</div>
<div v-if="node.isOpen.value" class="children">
<TreeNode
v-for="childId in tree.children.get(node.id)"
:key="childId"
:node="tree.get(childId)!"
:tree="tree"
/>
</div>
</div>
</template>
Selection Modes Comparison
// Cascade mode (default)
const cascade = createNested({ selection: 'cascade' })
cascade.register({ id: 'parent', value: 'Parent' })
cascade.register({ id: 'child', value: 'Child', parentId: 'parent' })
cascade.select('parent')
// Result: parent=selected, child=selected
// Independent mode
const independent = createNested({ selection: 'independent' })
independent.register({ id: 'parent', value: 'Parent' })
independent.register({ id: 'child', value: 'Child', parentId: 'parent' })
independent.select('parent')
// Result: parent=selected, child=unselected
// Leaf mode
const leaf = createNested({ selection: 'leaf' })
leaf.register({ id: 'parent', value: 'Parent' })
leaf.register({ id: 'child', value: 'Child', parentId: 'parent' })
leaf.select('parent')
// Result: parent=unselected, child=selected (child is leaf)
Flatten Tree
const tree = createNested()
tree.register({ id: 'root', value: 'Root' })
tree.register({ id: 'child-1', value: 'Child 1', parentId: 'root' })
tree.register({ id: 'child-2', value: 'Child 2', parentId: 'root' })
const flat = tree.toFlat()
// [
// { id: 'root', parentId: undefined, value: 'Root' },
// { id: 'child-1', parentId: 'root', value: 'Child 1' },
// { id: 'child-2', parentId: 'root', value: 'Child 2' }
// ]
Type Safety
interface FileNode extends NestedTicketInput {
name: string
type: 'file' | 'folder'
size?: number
}
const fileTree = createNested<FileNode>()
const folder = fileTree.register({
name: 'Documents',
type: 'folder'
})
const file = fileTree.register({
name: 'README.md',
type: 'file',
size: 1024,
parentId: folder.id
})
// Type-safe access
console.log(file.name) // string
console.log(file.type) // 'file' | 'folder'
console.log(file.size) // number | undefined
console.log(file.isLeaf.value) // boolean
Ticket-Level Methods
const tree = createNested()
const ticket = tree.register({ id: 'node', value: 'Node' })
// Open/close methods
ticket.open()
ticket.close()
ticket.flip()
// Activation
ticket.activate()
ticket.deactivate()
// Tree traversal
const path = ticket.getPath() // ID[]
const ancestors = ticket.getAncestors() // ID[]
const descendants = ticket.getDescendants() // ID[]
// Relationships
const sibs = ticket.siblings() // ID[]
const pos = ticket.position() // number
console.log(ticket.isAncestorOf('other-id')) // boolean
console.log(ticket.hasAncestor('ancestor-id')) // boolean
// State refs
console.log(ticket.isOpen.value) // boolean
console.log(ticket.isActive.value) // boolean
console.log(ticket.isLeaf.value) // boolean
console.log(ticket.depth.value) // number
Context Pattern
import { createNestedContext } from '@vuetify/v0'
export const [useFileTree, provideFileTree, fileTree] =
createNestedContext({ selection: 'cascade' })
// In parent component
provideFileTree()
// In child component
const tree = useFileTree()
tree.expandAll()
Performance
- Parent-child lookups: O(1) via Map
- Tree traversal: O(d) for getPath where d = depth
- Get descendants: O(n) breadth-first traversal
- Cascading selection: O(n) where n = descendants
- Open/close: O(1) Set operations
See Also
- createGroup - Base multi-selection system
- createSelection - Selection tracking
- createBreadcrumbs - Breadcrumb navigation