Tree
Accessible, unstyled, data-driven tree view with typed nodes, lazy loading, and virtualization.
|
|
| Package |
@vizejs/ui/tree |
| Maturity |
stable |
| Own the source |
vize lib pull tree |
| Requires |
collection, context, controllable-state, id, sortable, typeahead, virtualizer |
| Aliases |
tree view, treeview, file tree, hierarchy, outline, nested list |
| Covers |
WAI-ARIA tree view pattern, React Aria Tree, Ark UI Tree View, Reka UI Tree, MUI Tree View |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { Tree, TreeRoot, TreeItem, TreeItemCheckbox, TreeItemToggle } from "@vizejs/ui/tree";
Or copy the source into your project with vize lib pull tree (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Data-driven file tree with expand toggles and selection bound through v-model:selected. -->
<script setup lang="ts">
import { ref } from "vue";
import { TreeItem, TreeItemToggle, TreeRoot } from "@vizejs/ui/tree";
interface FileNode {
readonly id: string;
readonly name: string;
readonly children?: readonly FileNode[];
}
const files: readonly FileNode[] = [
{
id: "src",
name: "src",
children: [
{ id: "app", name: "App.vue" },
{ id: "main", name: "main.ts" },
],
},
{ id: "docs", name: "docs", children: [{ id: "guide", name: "guide.md" }] },
{ id: "readme", name: "README.md" },
];
const selected = ref<readonly string[]>(["app"]);
</script>
<template>
<div>
<TreeRoot
v-model:selected="selected"
aria-label="Project files"
:items="files"
:get-key="(node) => node.id"
:get-children="(node) => node.children"
:default-expanded="['src']"
>
<template #default="{ items }">
<TreeItem v-for="item in items" :key="item.key" :item>
<TreeItemToggle>
<template #default="{ expandable, expanded }">
<span v-if="expandable" aria-hidden="true">{{ expanded ? "-" : "+" }}</span>
</template>
</TreeItemToggle>
{{ item.node.name }}
</TreeItem>
</template>
</TreeRoot>
<output>Selected: {{ selected.join(", ") || "nothing" }}</output>
</div>
</template>
API
Tree<T, K extends TreeKey>
Source: src/families/data/tree/tree-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned tree base id. null and undefined select a deterministic fallback. |
items (required) |
readonly T[] |
required |
Root nodes in display order. |
getKey (required) |
(node: T) => K |
required |
Resolve a node's stable key. Keys must be unique across the whole tree. |
getChildren |
(node: T) => readonly T[] | null | undefined |
undefined |
Resolve a node's children. null and undefined mean "no children yet", which lets loadChildren fetch them lazily; an empty array marks a leaf. |
hasChildren |
(node: T) => boolean |
() => true |
Whether a node without resolved children may lazily load some. Only consulted with loadChildren. |
loadChildren |
(node: T, context: TreeLoadContext) => Promise<readonly T[]> |
undefined |
Asynchronously load a node's children the first time it expands. |
getTextValue |
(node: T) => string |
undefined |
Text used by typeahead. undefined reads the rendered row text, so virtualized trees should provide it to reach rows outside the window. |
isDisabled |
(node: T) => boolean |
undefined |
Disable individual nodes. Disabled rows stay focusable but cannot expand, select, or check. |
expanded |
readonly K[] |
undefined |
Controlled expanded keys (v-model:expanded). undefined selects uncontrolled behavior. |
defaultExpanded |
readonly K[] |
[] |
Initially expanded keys for uncontrolled use. |
selected |
readonly K[] |
undefined |
Controlled selected keys (v-model:selected). Single mode keeps at most one key. |
defaultSelected |
readonly K[] |
[] |
Initially selected keys for uncontrolled use. |
checked |
readonly K[] |
undefined |
Controlled checked keys (v-model:checked), normalized by checkPropagation. |
defaultChecked |
readonly K[] |
[] |
Initially checked keys for uncontrolled use. A cascading parent key checks its subtree. |
selectionMode |
TreeSelectionMode |
"single" |
Selection model: none, one key, or a set of keys. |
selectionFollowsFocus |
boolean |
false |
Whether arrow-key focus also selects in single selection mode. |
expandOnClick |
boolean |
false |
Whether clicking an expandable row also toggles it. |
checkable |
boolean |
false |
Expose aria-checked on rows and make Space toggle checkboxes instead of selection. |
checkPropagation |
TreeCheckPropagation |
"cascade" |
How checking a node affects its descendants and ancestors. |
disabled |
boolean |
false |
Disable the whole tree and remove it from sequential focus order. |
dir |
TreeDirection |
undefined |
Reading direction used to map ArrowRight and ArrowLeft to expand and collapse. undefined inherits DirectionProvider/LocaleProvider, then "ltr". |
typeahead |
boolean |
true |
Move focus to the next row whose text starts with typed characters. |
typeaheadTimeout |
number |
500 |
Idle time in milliseconds before typeahead starts a new query. |
virtualizer |
TreeVirtualizer |
undefined |
Windowing adapter from useTreeVirtualizer. The tree element becomes the scroll viewport. |
reorder |
TreeReorderController<K> |
undefined |
Drag and Alt+Arrow reorder adapter from useTreeReorder. |
ariaLabel |
string |
undefined |
Accessible name when no visible label or aria-labelledby supplies one. |
ariaLabelledby |
string |
undefined |
Space-separated ids that label the tree. |
ariaDescribedby |
string |
undefined |
Space-separated ids that describe the tree. |
Events
| Event |
Payload |
Description |
update:expanded |
[keys: readonly K[]] |
Fired with the next expanded keys whenever expansion changes. |
update:selected |
[keys: readonly K[]] |
Fired with the next selected keys whenever selection changes. |
update:checked |
[keys: readonly K[]] |
Fired with the next normalized checked keys whenever a checkbox changes. |
action |
[key: K, node: T, nativeEvent: Event] |
Fired when Enter or a double click activates a row, for example to open a file. |
load |
[key: K, children: readonly T[]] |
Fired after loadChildren resolves for a node. |
loadError |
[key: K, error: unknown] |
Fired when loadChildren rejects for a node. |
Slots
| Slot |
Slot props |
Description |
default |
TreeSlotState<T, K> |
TreeItem rows. Receives rendered rows, every visible row, and selection state. |
Exposed
| Member |
Type |
Description |
activeKey |
— |
|
checked |
— |
|
collapse |
— |
|
collapseAll |
— |
|
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
expand |
— |
|
expandAll |
— |
|
expanded |
— |
|
focus |
— |
|
focusKey |
— |
|
getCheckedState |
— |
|
id |
— |
|
reload |
— |
|
selected |
— |
|
setSelected |
— |
|
toggle |
— |
|
toggleChecked |
— |
|
TreeRoot<T, K extends TreeKey>
Source: src/families/data/tree/tree-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned tree base id. null and undefined select a deterministic fallback. |
items (required) |
readonly T[] |
required |
Root nodes in display order. |
getKey (required) |
(node: T) => K |
required |
Resolve a node's stable key. Keys must be unique across the whole tree. |
getChildren |
(node: T) => readonly T[] | null | undefined |
undefined |
Resolve a node's children. null and undefined mean "no children yet", which lets loadChildren fetch them lazily; an empty array marks a leaf. |
hasChildren |
(node: T) => boolean |
() => true |
Whether a node without resolved children may lazily load some. Only consulted with loadChildren. |
loadChildren |
(node: T, context: TreeLoadContext) => Promise<readonly T[]> |
undefined |
Asynchronously load a node's children the first time it expands. |
getTextValue |
(node: T) => string |
undefined |
Text used by typeahead. undefined reads the rendered row text, so virtualized trees should provide it to reach rows outside the window. |
isDisabled |
(node: T) => boolean |
undefined |
Disable individual nodes. Disabled rows stay focusable but cannot expand, select, or check. |
expanded |
readonly K[] |
undefined |
Controlled expanded keys (v-model:expanded). undefined selects uncontrolled behavior. |
defaultExpanded |
readonly K[] |
[] |
Initially expanded keys for uncontrolled use. |
selected |
readonly K[] |
undefined |
Controlled selected keys (v-model:selected). Single mode keeps at most one key. |
defaultSelected |
readonly K[] |
[] |
Initially selected keys for uncontrolled use. |
checked |
readonly K[] |
undefined |
Controlled checked keys (v-model:checked), normalized by checkPropagation. |
defaultChecked |
readonly K[] |
[] |
Initially checked keys for uncontrolled use. A cascading parent key checks its subtree. |
selectionMode |
TreeSelectionMode |
"single" |
Selection model: none, one key, or a set of keys. |
selectionFollowsFocus |
boolean |
false |
Whether arrow-key focus also selects in single selection mode. |
expandOnClick |
boolean |
false |
Whether clicking an expandable row also toggles it. |
checkable |
boolean |
false |
Expose aria-checked on rows and make Space toggle checkboxes instead of selection. |
checkPropagation |
TreeCheckPropagation |
"cascade" |
How checking a node affects its descendants and ancestors. |
disabled |
boolean |
false |
Disable the whole tree and remove it from sequential focus order. |
dir |
TreeDirection |
undefined |
Reading direction used to map ArrowRight and ArrowLeft to expand and collapse. undefined inherits DirectionProvider/LocaleProvider, then "ltr". |
typeahead |
boolean |
true |
Move focus to the next row whose text starts with typed characters. |
typeaheadTimeout |
number |
500 |
Idle time in milliseconds before typeahead starts a new query. |
virtualizer |
TreeVirtualizer |
undefined |
Windowing adapter from useTreeVirtualizer. The tree element becomes the scroll viewport. |
reorder |
TreeReorderController<K> |
undefined |
Drag and Alt+Arrow reorder adapter from useTreeReorder. |
ariaLabel |
string |
undefined |
Accessible name when no visible label or aria-labelledby supplies one. |
ariaLabelledby |
string |
undefined |
Space-separated ids that label the tree. |
ariaDescribedby |
string |
undefined |
Space-separated ids that describe the tree. |
Events
| Event |
Payload |
Description |
update:expanded |
[keys: readonly K[]] |
Fired with the next expanded keys whenever expansion changes. |
update:selected |
[keys: readonly K[]] |
Fired with the next selected keys whenever selection changes. |
update:checked |
[keys: readonly K[]] |
Fired with the next normalized checked keys whenever a checkbox changes. |
action |
[key: K, node: T, nativeEvent: Event] |
Fired when Enter or a double click activates a row, for example to open a file. |
load |
[key: K, children: readonly T[]] |
Fired after loadChildren resolves for a node. |
loadError |
[key: K, error: unknown] |
Fired when loadChildren rejects for a node. |
Slots
| Slot |
Slot props |
Description |
default |
TreeSlotState<T, K> |
TreeItem rows. Receives rendered rows, every visible row, and selection state. |
Exposed
| Member |
Type |
Description |
element |
HTMLDivElement | null |
Rendered tree element. |
id |
string |
Root-owned base id. |
expanded |
readonly K[] |
Currently expanded keys. |
selected |
readonly K[] |
Currently selected keys. |
checked |
readonly K[] |
Normalized checked keys. |
activeKey |
K | null |
Key owning roving focus. |
focus |
(options?: FocusOptions) => void |
Move focus to the active, selected, or first row. |
focusKey |
(key: K, options?: FocusOptions) => boolean |
Make a visible key active and focus it, scrolling a virtual window when needed. |
expand |
(key: K) => Promise<boolean> |
Expand one node, loading lazy children first when needed. |
collapse |
(key: K) => boolean |
Collapse one node. |
toggle |
(key: K) => Promise<boolean> |
Toggle one node's expansion. |
expandAll |
() => boolean |
Expand every node whose children are already resolved. |
collapseAll |
() => boolean |
Collapse every node. |
setSelected |
(keys: readonly K[]) => boolean |
Replace the selected keys. Single mode keeps at most the first key. |
toggleChecked |
(key: K) => boolean |
Toggle one node's checkbox with the configured propagation. |
getCheckedState |
(key: K) => TreeCheckedState |
Resolve the tri-state checkbox value of any known node. |
reload |
(key: K) => Promise<boolean> |
Reload lazy children for one node. |
TreeItem<T, K extends TreeKey>
Source: src/families/data/tree/tree-item.vue
Props
| Prop |
Type |
Default |
Description |
item (required) |
TreeFlatNode<T, K> |
required |
Visible row from the TreeRoot slot items. |
ariaLabel |
string |
undefined |
Accessible name when the row text does not supply one. |
ariaLabelledby |
string |
undefined |
Space-separated ids that label the row. |
ariaDescribedby |
string |
undefined |
Space-separated ids that describe the row. |
Slots
| Slot |
Slot props |
Description |
default |
TreeItemSlotState<T, K> |
Row content. Receives the typed node plus expansion, selection, and checkbox state. |
Exposed
| Member |
Type |
Description |
node |
T |
Consumer-owned node data. |
key |
K |
Stable node key. |
level |
number |
One-based depth. |
expandable |
boolean |
Whether the node has, or may lazily load, children. |
expanded |
boolean |
Whether the node is expanded. |
selected |
boolean |
Whether the node is selected. |
checked |
TreeCheckedState |
Tri-state checkbox value. |
disabled |
boolean |
Whether the node or the tree is disabled. |
active |
boolean |
Whether the node owns roving focus. |
loadState |
TreeLoadState |
Lazy children lifecycle for this node. |
state |
TreeItemState |
Stable expansion state token. |
element |
HTMLDivElement | null |
Rendered treeitem element. |
id |
string |
Deterministic treeitem id. |
focus |
(options?: FocusOptions) => void |
Make this row active and focus it. |
TreeItemCheckbox
Source: src/families/data/tree/tree-item-checkbox.vue
Slots
| Slot |
Slot props |
Description |
default |
{ /** Tri-state checkbox value of the owning row. */ readonly checked: TreeCheckedState; /** Whether the owning row or the tree is disabled. */ readonly disabled: boolean; } |
Checkbox indicator content. Receives the tri-state value of the owning row. |
TreeItemToggle
Source: src/families/data/tree/tree-item-toggle.vue
Slots
| Slot |
Slot props |
Description |
default |
{ /** Whether the owning row has, or may lazily load, children. */ readonly expandable: boolean; /** Whether the owning row is expanded. */ readonly expanded: boolean; /** Lazy children lifecycle for the owning row. */ readonly loadState: TreeLoadState; /** Stable expansion state token. */ readonly state: TreeItemState; } |
Expand indicator content, for example a chevron. Receives expansion and load state. |
Behavior
Normative state x input -> outcome table for tree-root.vue, tree-item.vue,
tree-item-toggle.vue, and tree-item-checkbox.vue (@vizejs/ui/tree), plus
the useTreeVirtualizer and useTreeReorder adapters. Every row is proven by
the named test in tree.test.ts, tree-adapters.test.ts, tree-model.test.ts,
or tree-ssr.test.ts; compile-only guarantees live in tree.types.test-d.ts.
The tree follows the WAI-ARIA APG tree view pattern. It renders flat: every
visible node is one role="treeitem" row carrying aria-level,
aria-setsize, and aria-posinset, so DOM order equals visual order and rows
can be virtualized without nested role="group" containers. Nodes are
consumer-owned data of any type T; getKey and getChildren are typed
against T, and the key type K is inferred from getKey and flows into every
v-model, emit, slot, and exposed method.
| ID |
State |
Input |
Outcome |
Evidence |
| T1 |
default |
render |
tree with flat treeitem rows, levels, set positions, aria-expanded on parents only, roving tabindex |
renders APG tree semantics with flat levels, set positions, and roving tabindex |
| T2 |
focus on a row |
ArrowDown/Up/Right/Left, Home, End |
APG movement: Right expands then enters, Left collapses then moves to the parent, Home/End jump |
arrow keys, Home, and End follow the APG tree keyboard model |
| T3 |
focus inside a branch |
collapse an ancestor |
focus and the active key move to the collapsed ancestor instead of being lost |
collapsing an ancestor moves focus from a hidden descendant to the ancestor |
| T4 |
dir="rtl" |
ArrowLeft / ArrowRight |
horizontal expand and collapse keys swap |
RTL maps ArrowLeft to expand and ArrowRight to collapse |
| T5 |
single selection |
click, Enter, focus movement |
click and Enter select one key; Enter and double click emit action; selectionFollowsFocus opt-in |
single selection follows click and Enter, and emits action for activation |
| T6 |
multiple selection |
click, Shift+click, Space, Shift+Arrow, Ctrl/Cmd+A |
toggles, anchor ranges, focus extension, and select-all/clear-all with aria-multiselectable |
multiple selection toggles, extends ranges, and selects all |
| T7 |
selectionMode="none" |
click, Enter |
no aria-selected, no selection emits; action still fires |
selection mode none omits aria-selected and ignores selection input |
| T8 |
checkable cascade |
Space, checkbox click |
checks cascade to descendants; ancestors derive checked/mixed/unchecked in aria-checked |
checkboxes cascade to descendants and derive tri-state ancestors |
| T9 |
checkPropagation="independent" |
Space |
only the requested node toggles |
independent checkboxes toggle only the requested node |
| T10 |
lazy branch |
expand |
loadChildren runs once, row is aria-busy while loading, children inherit a checked parent, cached |
lazy children load on expand with busy state, load events, and inherited checks |
| T11 |
lazy branch |
load rejects, then reload() |
data-load-state="error" and loadError; reload retries and renders children |
failed lazy loads expose an error state and can reload |
| T12 |
focus on a row |
printable characters |
typeahead focuses the next row whose text matches, using the shared typeahead buffer |
typeahead moves focus to the next row whose text matches |
| T13 |
any |
*, expandAll(), collapseAll() |
* expands the focused row's siblings; expand-all covers every resolved branch |
asterisk expands siblings and expandAll/collapseAll cover every resolved branch |
| T14 |
disabled row / disabled tree |
click, keys, Tab |
disabled rows stay focusable but never expand, select, or check; a disabled tree leaves the tab order |
disabled rows stay focusable but cannot expand, select, or check |
| T15 |
controlled expanded/selected |
user input |
update events fire while rendered state waits for the parent |
controlled expansion and selection wait for the parent to accept requests |
| T16 |
toggle part / expandOnClick |
click |
the toggle expands without selecting and keeps focus on the row |
the toggle expands on click without selecting, and expandOnClick toggles rows |
| T17 |
exposed instance |
imperative calls |
typed focus, focusKey, expand, toggle, setSelected, toggleChecked, getCheckedState |
exposes typed state and imperative focus, selection, and checkbox controls |
| T18 |
missing provider |
setup |
compound parts fail closed with the shared context diagnostic |
compound parts require a matching root provider |
| T19 |
useTreeVirtualizer |
render |
only the window renders, every row still reports its full-tree set metadata |
a virtualizer renders only the window and keeps set metadata for every row |
| T20 |
useTreeVirtualizer |
End, typeahead |
focus scrolls unmounted rows into the window first; typeahead reaches them through getTextValue |
keyboard focus scrolls rows outside the window into view before focusing them |
| T21 |
active row scrolled out |
Tab into the tree |
the tree element becomes the tab stop and forwards focus to the active row |
the tree element takes sequential focus while the active row is outside the window |
| T22 |
useTreeReorder |
Alt+ArrowUp/Down/Right/Left |
onMove receives sibling, nesting, and outdent requests; data is never mutated by the tree |
Alt+Arrow keys request sibling, nesting, and outdent moves |
| T23 |
useTreeReorder with canMove |
move request |
vetoed and self-targeted moves are dropped; rows register with the shared pointer sortable engine |
canMove vetoes moves and rows register with the pointer sort engine |
| T24 |
model helpers |
index, flatten, derive, toggle |
pre-order indexing, duplicate-key diagnostic, tri-state derivation, disabled-aware cascade |
tree-model.test.ts |
| T25 |
SSR and hydration |
isolated render/mount |
byte-identical markup, virtual window from initialRect, hydration without warnings |
tree-ssr.test.ts |
Notes
- Lazy branches that start expanded load after mount, so server and client
render the same idle markup before any request runs.
- The toggle and checkbox parts are
aria-hidden pointer affordances: keyboard
users expand with arrows and check with Space on the row itself, which keeps
interactive controls from nesting inside a treeitem.
- Keys are compared with
Object.is; 1 and "1" are distinct keys but must
not both appear in one tree when reordering, because the drag engine keys
rows by String(key).
- No styles ship with the primitive. Rows expose
data-state, data-level,
data-selected, data-checked, data-load-state, data-dragging, and
data-drop-position; indentation and virtual offsets are consumer-owned.