Tree
Overview
A component for displaying hierarchical tree data with selection and expand/collapse support. STree combines the Aria TreeRoot family of base primitives (TreeRoot/TreeItem/TreeVirtualizerRoot/TreeVirtualizerItem, zero-style) with the TreeRoot context (controlled/uncontrolled selection and expansion, single/multiple selection, cascading selection, Roving Focus keyboard navigation); STreeVirtualizer layers virtualization on top to smoothly handle 1000+ nodes. Node content is fully customized through the item slot — icons, checkboxes, indentation, and connector lines are all injected by the consumer.
Usage
Features
- 🌳 Hierarchical data model —
items: TreeItemData<T>[](valueplus optional recursivechildren); theTreeItemData<T>generic preserves custom field types - 🎛️ Controlled/uncontrolled dual channels —
modelValue/defaultValuecontrol selection,expanded/defaultExpandedcontrol expansion; both supportv-modelandupdate:*events - ☑️ Single and multiple selection —
multiple(linked to themodelValuetype: single string ↔ array) toggles modes;selectionBehavior: 'toggle' | 'replace'controls multi-select clicks;Shift + Arrowperforms contiguous range selection - 🧩 Cascading selection —
propagateSelect(selecting a parent cascades to descendants),bubbleSelect(a parent auto-selects when all children are selected),allowParentSelect(whether parent nodes can be selected directly) - 📂 Expansion strategy —
toggleBehavior: 'multiple' | 'single'(multiple expanded nodes by default;singlecollapses the previous node when expanding a new one, accordion-like) - ⌨️ Keyboard navigation — Roving Focus:
↑/↓move focus,→/←expand/collapse or enter/return to a parent level,Enter/Spaceselect, character typeahead for quick location;loop(defaulttrue) wraps around; full RTL support - ⚡ Virtualization —
STreeVirtualizer+heightrenders only visible nodes, staying smooth with 1000+ nodes - ♿ Accessibility —
role="tree"/treeitemwitharia-expanded/aria-selected/aria-level/aria-setsize/aria-posinset/aria-multiselectable/aria-disabled, plusdata-vean-tree-*data attributes
Component family
STree(styled) — entry wrapper;TreeRootProps<T, U, M>generic passthrough +useForwardListenersevent merging +top/item/bottomslots;withDefaultsmirrors the Arialoop: truedefaultSTreeVirtualizer(styled) — virtualized wrapper; forwardscontentProps/dynamicContentPropstoVirtualizerContent; theitemslot additionally exposesvirtualItemTreeRoot(Aria) — root component;useControllableStatemanages selection/expansion,useSelectionBehaviorhandles single/multi/range selection,useRovingFocusGroup+useTypeaheadimplement keyboard navigation;provideTreeRootContextbridges child itemsTreeItem(Aria) — single node;useRovingFocusGroupItemmanages focus, rendersaria-*anddata-*attributes, dispatchesselect/toggleevents viahandleAndDispatchCustomEventTreeVirtualizerRoot(Aria) — virtualized root;VirtualizerRoot+ TanStack Virtual, forwards flattenedflattenItemsTreeVirtualizerItem(Aria) — virtualized node; combinesTreeItem+VirtualizerItem(data-vean-tree-virtualizer-item)
Demos
Basic
- composables
- components
- home
- app.vue
- nuxt.config.ts
Directory Structure
Virtualizer
- 01 Basic — a basic directory tree (
items+default-expanded+topslot) - 02 Virtualizer —
STreeVirtualizerrendering 1000+ nodes with virtualization
API
Aria Layer
6Tree<T>
Slots
| Slot Name | Parameters | Description |
|---|---|---|
top | — | Content rendered before the tree items. |
bottom | — | Content rendered after the tree items. |
item | { item: FlattenedItem<T>; modelValue: MaybeArray<string> | undefined; expanded: string[]; } | Content for a single tree item. |
TreeItem
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Value associated with the current item. |
level | number | — | Level. |
disabled | boolean | — | When `true`, prevents the user from selecting or toggling the item. |
disabledSelect | boolean | — | When `true`, prevents the user from selecting the item. |
disabledToggle | boolean | — | When `true`, prevents the user from toggling the item. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
select | TreeSelectEvent<string> | Event handler called when selecting item. |
toggle | TreeToggleEvent<string> | Event handler called when toggling item. |
TreeRoot<T, U, M>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | U | — | The controlled value of the tree. Can be bound-with with `v-model`. |
defaultValue | U | — | The value of the tree when initially rendered. Use when you do not need to control the state of the tree |
multiple | IsMultiple<U, M> | — | Determines whether a "single" or "multiple" items can be selected at a time. |
items | T[] | — | List of items |
expanded | string[] | — | The controlled value of the expanded item. Can be bound-with with `v-model`. |
defaultExpanded | string[] | — | The value of the expanded tree when initially rendered. |
selectionBehavior | TreeSelectBehavior | — | How multiple selection should behave in the collection. |
toggleBehavior | TreeToggleBehavior | 'multiple' | Determines whether a "single" or "multiple" items can be toggled at a time. |
dir | Direction | — | The reading direction. |
loop | boolean | true | When `true`, keyboard navigation will loop from last item to first, and vice versa. |
disabled | boolean | — | When `true`, prevents the user from interacting with tree |
propagateSelect | boolean | — | When `true`, selecting parent will select the descendants. |
bubbleSelect | boolean | — | When `true`, selecting children will update the parent state. |
allowParentSelect | boolean | — | When `true`, parent can be selected. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | M extends true ? string[] : string | Emitted when the model value changes. |
update:expanded | string[] | Emitted when the expanded state changes. |
TreeVirtualizer<T, U, M>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
contentProps | VirtualizerContentProps | — | Properties forwarded to the content element. |
dynamicContentProps | VirtualizerDynamicContentProps | — | Properties forwarded to the dynamic content element. |
animated | boolean | false | When `true`, renders visible items in normal document flow (dynamic mode) so
item positions stay correct across expand/collapse without absolute offsets. |
modelValue | U | — | The controlled value of the tree. Can be bound-with with `v-model`. |
defaultValue | U | — | The value of the tree when initially rendered. Use when you do not need to control the state of the tree |
multiple | IsMultiple<U, M> | — | Determines whether a "single" or "multiple" items can be selected at a time. |
items | T[] | — | List of items |
expanded | string[] | — | The controlled value of the expanded item. Can be bound-with with `v-model`. |
defaultExpanded | string[] | — | The value of the expanded tree when initially rendered. |
selectionBehavior | TreeSelectBehavior | — | How multiple selection should behave in the collection. |
toggleBehavior | TreeToggleBehavior | 'multiple' | Determines whether a "single" or "multiple" items can be toggled at a time. |
dir | Direction | — | The reading direction. |
loop | boolean | true | When `true`, keyboard navigation will loop from last item to first, and vice versa. |
disabled | boolean | — | When `true`, prevents the user from interacting with tree |
propagateSelect | boolean | — | When `true`, selecting parent will select the descendants. |
bubbleSelect | boolean | — | When `true`, selecting children will update the parent state. |
allowParentSelect | boolean | — | When `true`, parent can be selected. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
height | string | number | — | The height of the virtualizer root |
options | VirtualizerOptions | — | Options. |
dynamic | boolean | — | Whether dynamic. |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | M extends true ? string[] : string | Emitted when the model value changes. |
update:expanded | string[] | Emitted when the expanded state changes. |
TreeVirtualizerItem
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Value associated with the current item. |
level | number | — | Level. |
disabled | boolean | — | When `true`, prevents the user from selecting or toggling the item. |
disabledSelect | boolean | — | When `true`, prevents the user from selecting the item. |
disabledToggle | boolean | — | When `true`, prevents the user from toggling the item. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
data | VirtualItem | — | Data. |
customStyle | boolean | — | Whether custom style. |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
select | TreeSelectEvent<string> | Event handler called when selecting item. |
toggle | TreeToggleEvent<string> | Event handler called when toggling item. |
TreeVirtualizerRoot<T, U, M>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | U | — | The controlled value of the tree. Can be bound-with with `v-model`. |
defaultValue | U | — | The value of the tree when initially rendered. Use when you do not need to control the state of the tree |
multiple | IsMultiple<U, M> | — | Determines whether a "single" or "multiple" items can be selected at a time. |
items | T[] | — | List of items |
expanded | string[] | — | The controlled value of the expanded item. Can be bound-with with `v-model`. |
defaultExpanded | string[] | — | The value of the expanded tree when initially rendered. |
selectionBehavior | TreeSelectBehavior | — | How multiple selection should behave in the collection. |
toggleBehavior | TreeToggleBehavior | 'multiple' | Determines whether a "single" or "multiple" items can be toggled at a time. |
dir | Direction | — | The reading direction. |
loop | boolean | true | When `true`, keyboard navigation will loop from last item to first, and vice versa. |
disabled | boolean | — | When `true`, prevents the user from interacting with tree |
propagateSelect | boolean | — | When `true`, selecting parent will select the descendants. |
bubbleSelect | boolean | — | When `true`, selecting children will update the parent state. |
allowParentSelect | boolean | — | When `true`, parent can be selected. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
height | string | number | — | The height of the virtualizer root |
options | VirtualizerOptions | — | Options. |
dynamic | boolean | — | Whether dynamic. |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | M extends true ? string[] : string | Emitted when the model value changes. |
update:expanded | string[] | Emitted when the expanded state changes. |
Notes
Architecture and benchmark differences
TreeRoot owns all state (selection/expansion through the useControllableState controlled/uncontrolled dual channels) and the selection strategy (useSelectionBehavior’s toggle/replace/range selection); all base primitives stay zero-style. STree/STreeVirtualizer only mirror the loop default and pass through slots — the node visuals are fully composed inside the item slot via STreeItem/STreeVirtualizerItem (indentation, icons, checkboxes, focus states all injected through classes). Keyboard navigation builds on Roving Focus (↑/↓ move, →/← expand/collapse, typeahead) and additionally supports Shift range selection — an interaction beyond the default contract of most mainstream tree components. Virtualization builds on the @vean/aria virtualizer and renders only visible nodes.
| Capability | VeanUI | Ant Design | Element Plus | Naive UI |
|---|---|---|---|---|
| Aria/style separation | ✅ | — | — | — |
| Single/multiple selection (toggle/replace) | ✅ | ✅ | ✅ | ✅ |
| Expansion strategy (single/multiple toggle) | ✅ | ✅ | ✅ | ✅ |
| Cascading selection (propagate/bubble/parent) | ✅ | ✅ | ✅ | ⚠️ |
| Keyboard navigation (arrows + loop + typeahead) | ✅ | ✅ | ✅ | ⚠️ |
| Virtualization (1k nodes) | ✅ | ✅ | ⚠️ | ✅ |
| Checkable mode (checkbox) | ⚠️ | ✅ | ✅ | ✅ |
| Draggable reordering | — | ✅ | ✅ | ✅ |
| Async child loading (loadData) | — | ✅ | ✅ | ✅ |
| Search filtering (searchValue) | — | ✅ | ✅ | — |
⚠️ = partial support (VeanUI checkable can be built via multiple + a custom item slot checkbox icon + the data-selected/data-contains-selected states; Naive UI has no built-in range selection or character location).
Cautions
- The
modelValuetype is tied tomultiple: a single-stringmodelValueforces single-select; an array withmultipletruthy enables multi-select (enforced by theIsMultiple<U, M>conditional type). - The
itemslot is free composition: the consumer must renderSTreeItem(orSTreeVirtualizerItem) passingitem.value/item.level; the slot also exposesitem.data(the raw node data) andisExpanded/isSelected/isIndeterminate/hasChildrenstates. - Parent nodes are not selectable by default (
allowParentSelectdefaults tofalse); enable it explicitly when needed. propagateSelect/bubbleSelectonly take effect in multi-select mode (multiple+ array value); with both on, the semi-checked state is expressed viadata-contains-selected/isIndeterminate.- With
toggleBehavior: 'single', expansion is accordion-like — only one branch stays expanded (findParentPathrebuilds the path). loopdefaults totrue(keyboard focus wraps around); bothSTreeandSTreeVirtualizermirror this default in their wrappers, so no extra work is needed when using the AriaTreeRootdirectly.- Virtualization requires a
height; theSTreeVirtualizeritemslot additionally providesvirtualItem(for:datapassthrough and absolute-position styling). - Expand and collapse animate with a height-collapsing transition on the moved subtree.
STreeVirtualizerhas an opt-inanimatedprop that renders visible items in normal document flow (dynamic mode) so item positions stay correct across expand/collapse without absolute offsets — it is off by default since dynamic measuring is heavier for very large datasets, and while a motion runs the transition list renders fully in document flow before windowing resumes. - Node text is fully provided by slots — the component has no hardcoded copy, so no localization is needed.
FAQ
How do I build a checkable tree?
Use multiple with a custom item slot that renders a checkbox icon and consumes the isSelected/isIndeterminate states:
<STree :items="items" multiple>
<template #item="{ item, isSelected, isIndeterminate }">
<STreeItem :value="item.value" :level="item.level">
<SIcon :icon="isIndeterminate ? 'lucide:minus' : isSelected ? 'lucide:check-square' : 'lucide:square'" />
{{ item.data.label }}
</STreeItem>
</template>
</STree>Add propagateSelect to cascade parent selection to descendants, or bubbleSelect to auto-select a parent when all its children are selected.
How do I control the expansion state?
Controlled: v-model:expanded="expandedKeys" (array of strings); or uncontrolled with :default-expanded="['node-1']". The expansion event is update:expanded.
How do I optimize for large data?
Use STreeVirtualizer with a height: only visible nodes render (customize options.estimateSize for row height), staying smooth with 1000+ nodes.
<STreeVirtualizer height="360px" :items="items">
<template #item="{ item, virtualItem }">
<STreeVirtualizerItem :value="item.value" :level="item.level" :data="virtualItem">
{{ item.data.label }}
</STreeVirtualizerItem>
</template>
</STreeVirtualizer>How do I prevent selecting parent nodes?
allowParentSelect defaults to false — clicking a parent with children only expands/collapses it without selecting. Set :allow-parent-select="true" explicitly to allow selecting parents.
How do I load children asynchronously?
There is no built-in loadData contract; load lazily in the toggle event and update items:
<STree :items="items" @toggle="event => loadChildren(event.value)">
<!-- item slot -->
</STree>Write children into the matching node of items once loaded (expansion is tracked by expanded).
How do I customize node icons and indentation?
The item slot composes freely: compute padding-left from item.level, switch icons on item.hasChildren/isExpanded, and style the focus state with focus:ring-* classes — see demo 01.
How does the keyboard work?
↑/↓ move focus across visible nodes; → expands a collapsed node (or enters a child level), ← collapses (or returns to the parent); Enter/Space select the current node; typing characters triggers typeahead location; Shift + ↑/↓ performs range selection in multi-select mode; with loop on (default), focus wraps around.