createNested
The createNested composable extends createGroup to manage hierarchical tree structures. It provides parent-child relationship tracking, open/close state management, tree traversal utilities, and pluggable open strategies.
Usage
The createNested composable manages hierarchical tree structures with parent-child relationships, open/close states, and tree traversal.
<script setup lang="ts">
import { createNested } from '@vuetify/v0'
import { computed } from 'vue'
const nav = createNested()
// Register navigation with inline children
nav.onboard([
{
id: 'getting-started',
value: 'Getting Started',
children: [
{ id: 'installation', value: 'Installation' },
{ id: 'quick-start', value: 'Quick Start' },
{ id: 'typescript', value: 'TypeScript Support' },
],
},
{
id: 'components',
value: 'Components',
children: [
{ id: 'buttons', value: 'Buttons' },
{
id: 'inputs',
value: 'Inputs',
children: [
{ id: 'text-field', value: 'Text Field' },
{ id: 'checkbox', value: 'Checkbox' },
{ id: 'radio', value: 'Radio' },
],
},
{ id: 'selection', value: 'Selection' },
],
},
{
id: 'composables',
value: 'Composables',
children: [
{ id: 'use-theme', value: 'useTheme' },
{ id: 'use-form', value: 'useForm' },
{ id: 'use-nested', value: 'useNested' },
],
},
{ id: 'changelog', value: 'Changelog' },
])
nav.open('getting-started')
const stats = computed(() => ({
total: nav.size,
opened: nav.openedIds.size,
selected: nav.selectedIds.size,
}))
function getVisibleNodes (parentId?: string): string[] {
const ids = parentId
? nav.children.get(parentId) ?? []
: nav.roots.value.map(r => r.id)
return ids.flatMap(id => {
const result = [id]
if (nav.opened(id)) {
result.push(...getVisibleNodes(id))
}
return result
})
}
const visibleNodes = computed(() => getVisibleNodes())
function onCheckboxClick (e: Event, id: string) {
e.stopPropagation()
nav.toggle(id)
}
</script>
<template>
<div class="flex flex-col gap-3">
<div class="flex gap-2 flex-wrap">
<button
class="px-3 py-1 border border-divider rounded hover:bg-surface-tint text-sm"
@click="nav.expandAll()"
>
Expand All
</button>
<button
class="px-3 py-1 border border-divider rounded hover:bg-surface-tint text-sm"
@click="nav.collapseAll()"
>
Collapse All
</button>
<span class="text-on-surface opacity-60 text-sm ml-auto">
{{ stats.selected }} selected / {{ stats.opened }} open / {{ stats.total }} total
</span>
</div>
<nav class="border border-divider rounded overflow-hidden">
<div
v-for="id in visibleNodes"
:key="id"
class="flex items-center gap-2 py-2 hover:bg-surface-tint border-b border-divider last:border-b-0"
:class="{ 'cursor-pointer': !nav.isLeaf(id) }"
:style="{ paddingLeft: `${nav.getDepth(id) * 16 + 12}px` }"
@click="nav.flip(id)"
>
<span
v-if="!nav.isLeaf(id)"
class="w-4 text-center opacity-60"
>
{{ nav.opened(id) ? '-' : '+' }}
</span>
<span v-else class="w-4" />
<button
class="size-4 border rounded inline-flex items-center justify-center border-divider text-xs"
:class="nav.selected(id) ? 'bg-primary border-primary text-on-primary' : nav.mixed(id) ? 'bg-primary border-primary text-on-primary' : ''"
@click="onCheckboxClick($event, id)"
>
<span v-if="nav.selected(id)">✓</span>
<span v-else-if="nav.mixed(id)">-</span>
</button>
<span
class="text-sm"
:class="nav.isLeaf(id) ? 'opacity-80' : 'font-medium'"
>
{{ nav.get(id)?.value }}
</span>
</div>
</nav>
</div>
</template>
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 | |
roots | Computed, root nodes | |
leaves | Computed, leaf nodes | |
ticket.isOpen | Ref via toRef() | |
ticket.isLeaf | Ref via toRef() | |
ticket.depth | Ref via toRef() |
Architecture
createNested extends createGroup with hierarchical tree management:
Functions
createNested
(_options?: NestedOptions) => RCreates a new nested tree instance with hierarchical management. Extends `createGroup` to support parent-child relationships, tree traversal, and open/close state management. Perfect for tree views, nested navigation, and hierarchical data structures.
createNestedContext
(_options?: NestedContextOptions) => ContextTrinity<R>Creates a new nested context with provide/inject pattern.
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' })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' })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 selectedIdsCustom Open Strategies
For advanced use cases, implement custom strategies:
import type { OpenStrategy, OpenStrategyContext } from '@vuetify/v0'
const keepParentsOpenStrategy: OpenStrategy = {
onOpen: (id, context) => {
// context.openedIds - reactive Set of open node IDs
// context.children - Map of parent ID to child IDs
// context.parents - Map of child ID to parent ID
},
onClose: (id, context) => {
// Called after a node is closed
},
}
const tree = createNested({ openStrategy: keepParentsOpenStrategy }) The openStrategy option overrides open when provided. Use open for simple cases.
Convenience 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)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 descendantsContext 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()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.