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')Architecture
createNested extends createGroup with hierarchical tree management:
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() |
Examples
Collapsible Navigation Tree
A nested tree with expand/collapse all, multi-select checkboxes, and visual parent/child relationship indicators.
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' })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()Functions
createNested
(_options?: NestedOptions) => NestedContext<NestedTicketInput<unknown>, NestedTicket<NestedTicketInput<unknown>>>Creates 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<NestedContext<NestedTicketInput<unknown>, NestedTicket<NestedTicketInput<unknown>>>>Creates a new nested context with provide/inject pattern.
useNested
(namespace?: string) => NestedContext<NestedTicketInput<unknown>, NestedTicket<NestedTicketInput<unknown>>>Returns the current nested instance from context.
Options
disabled
MaybeRefOrGetter<boolean> | undefinedDisabled state for the entire model instance
Default: false
multiple
MaybeRefOrGetter<boolean> | undefinedAllow multiple tickets to be selected simultaneously
Default: false
mandatory
MaybeRefOrGetter<boolean | "force"> | undefinedControls mandatory selection behavior: - `false` (default): No mandatory selection enforcement - `true`: Prevents deselecting the last selected item - `'force'`: Automatically selects the first non-disabled item on registration
open
NestedOpenMode | undefinedControls how nodes expand/collapse. - `'multiple'` (default): Multiple nodes can be open simultaneously - `'single'`: Only one node open at a time (accordion behavior)
openAll
boolean | undefinedWhen true, parent nodes automatically open when children are registered. Similar to `enroll` in selection composables but for open state.
reveal
boolean | undefinedWhen true, opening a node also opens all its ancestors. Ensures the opened node is always visible in the tree.
selection
NestedSelectionMode | undefinedControls how selection cascades through the hierarchy. - `'cascade'` (default): Selecting parent selects descendants; ancestors show mixed state - `'independent'`: Each node selected independently - `'leaf'`: Only leaf nodes selectable; parent selection selects leaf descendants
active
NestedActiveMode | undefinedControls how many items can be active/highlighted simultaneously. - `'single'` (default): Only one item active at a time - `'multiple'`: Multiple items can be active simultaneously
openStrategy
OpenStrategy | undefinedAdvanced: Custom strategy for open behavior. Overrides `open` option if provided.
Properties
selectedValues
ComputedRef<Set<E["value"] extends Ref<infer U, infer U> ? U : E["value"]>>Computed Set of selected ticket values
selectedIndexes
ComputedRef<Set<number>>children
ReadonlyMap<ID, readonly ID[]>Map of parent IDs to arrays of child IDs. Use register/unregister to modify.
parents
ReadonlyMap<ID, ID | undefined>Map of child IDs to their parent ID (or undefined for roots). Use register/unregister to modify.
activeIds
Reactive<Set<ID>>Reactive Set of active/highlighted item IDs. Use activate/deactivate to modify.
Methods
move
(id: ID, toIndex: number) => E | undefinedSeek for a ticket based on direction and optional predicate
seek
(direction?: "first" | "last", from?: number, predicate?: (ticket) => boolean) => E | undefinedon
<K extends Extensible<RegistryEventName>>(event: K, cb: EventHandler<E, K>) => voidListen for registry events
off
<K extends Extensible<RegistryEventName>>(event: K, cb: EventHandler<E, K>) => voidStop listening for registry events
emit
<K extends Extensible<RegistryEventName>>(event: K, data: EventPayload<E, K>) => voidEmit an event with data
batch
<R>(fn: () => R) => RExecute operations in a batch, deferring cache invalidation and event emission until complete
reveal
(ids: ID | ID[]) => voidReveal node(s) by opening all ancestors (makes node visible without opening it)
toFlat
() => Array<{ id: ID; parentId: ID | undefined; value: Z extends NestedTicketInput<infer V> ? V : unknown; }>Convert tree to flat array with parentId references
isAncestorOf
(ancestorId: ID, descendantId: ID) => booleanCheck if ancestorId is an ancestor of descendantId
hasAncestor
(id: ID, ancestorId: ID) => booleanCheck if id has ancestorId as an ancestor (semantic alias for isAncestorOf)
position
(id: ID) => numberGet 1-indexed position among siblings (for aria-posinset). Returns 0 if not found.
unselect
(ids: ID | ID[]) => voidUnselect item(s) and all descendants, updating ancestor mixed states
register
(registration?: NestedRegistration<Z>) => ERegister a node with optional inline children (accepts input type, returns output type)
onboard
(registrations: NestedRegistration<Z>[]) => E[]Batch register nodes with optional inline children
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.