Tree
Alpha- composables
- components
- Home
- app.vue
- nuxt.config.ts
Directory Structure
<script setup lang="ts">
import { TreeItem, TreeRoot } from 'radix-vue'
import { Icon } from '@iconify/vue'
const items = [
{
title: 'composables',
icon: 'lucide:folder',
children: [
{ title: 'useAuth.ts', icon: 'vscode-icons:file-type-typescript' },
{ title: 'useUser.ts', icon: 'vscode-icons:file-type-typescript' },
],
},
{
title: 'components',
icon: 'lucide:folder',
children: [
{
title: 'Home',
icon: 'lucide:folder',
children: [
{ title: 'Card.vue', icon: 'vscode-icons:file-type-vue' },
{ title: 'Button.vue', icon: 'vscode-icons:file-type-vue' },
],
},
],
},
{ title: 'app.vue', icon: 'vscode-icons:file-type-vue' },
{ title: 'nuxt.config.ts', icon: 'vscode-icons:file-type-nuxt' },
]
</script>
<template>
<TreeRoot
v-slot="{ flattenItems }"
class="list-none select-none w-56 bg-white text-blackA11 rounded-lg p-2 text-sm font-medium"
:items="items"
:get-key="(item) => item.title"
:default-expanded="['components']"
>
<h2 class="font-semibold !text-base text-blackA11 px-2 pt-1">
Directory Structure
</h2>
<TreeItem
v-for="item in flattenItems"
v-slot="{ isExpanded }"
:key="item._id"
:style="{ 'padding-left': `${item.level - 0.5}rem` }"
v-bind="item.bind"
class="flex items-center py-1 px-2 my-0.5 rounded outline-none focus:ring-grass8 focus:ring-2 data-[selected]:bg-grass4"
>
<template v-if="item.hasChildren">
<Icon
v-if="!isExpanded"
icon="lucide:folder"
class="h-4 w-4"
/>
<Icon
v-else
icon="lucide:folder-open"
class="h-4 w-4"
/>
</template>
<Icon
v-else
:icon="item.value.icon || 'lucide:file'"
class="h-4 w-4"
/>
<div class="pl-2">
{{ item.value.title }}
</div>
</TreeItem>
</TreeRoot>
</template>
Features
- Can be controlled or uncontrolled.
- Focus is fully managed.
- Full keyboard navigation.
- Supports Right to Left direction.
- Supports multiple selection.
- Different selection behavior.
Installation
Install the component from your command line.
$ npm add radix-vue
Anatomy
Import all parts and piece them together.
<script setup>
import { TreeItem, TreeRoot, TreeVirtualizer } from 'radix-vue'
</script>
<template>
<TreeRoot>
<TreeItem />
<!-- or with virtual -->
<TreeVirtualizer>
<TreeItem />
</TreeVirtualizer>
</TreeRoot>
</template>
API Reference
Root
Contains all the parts of a tree.
Prop | Default | Type |
---|---|---|
as | 'ul' | AsTag | Component The element or component this component should render as. Can be overwrite by |
asChild | boolean Change the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details. | |
defaultExpanded | string[] The value of the expanded tree when initially rendered. Use when you do not need to control the state of the expanded tree | |
defaultValue | Record<string, any> | Record<string, any>[] The value of the tree when initially rendered. Use when you do not need to control the state of the tree | |
dir | 'ltr' | 'rtl' The reading direction of the listbox when applicable. | |
disabled | boolean When | |
expanded | string[] The controlled value of the expanded item. Can be binded with with | |
getChildren | val.children | ((val: Record<string, any>) => Record<string, any>[]) This function is passed the index of each item and should return a list of children for that item |
getKey* | (val: Record<string, any>) => string This function is passed the index of each item and should return a unique key for that item | |
items | Record<string, any>[] List of items | |
modelValue | Record<string, any> | Record<string, any>[] The controlled value of the tree. Can be binded with with | |
multiple | boolean Whether multiple options can be selected or not. | |
propagateSelect | boolean When | |
selectionBehavior | 'toggle' | 'toggle' | 'replace' How multiple selection should behave in the collection. |
Emit | Payload |
---|---|
update:expanded | [val: string[]] |
update:modelValue | [val: Record<string, any>] Event handler called when the value changes. |
Slots (default) | Payload |
---|---|
flattenItems | FlattenedItem<Record<string, any>>[] |
modelValue | Record<string, any> | Record<string, any>[] |
expanded | string[] |
Item
The item component.
Prop | Default | Type |
---|---|---|
as | 'li' | AsTag | Component The element or component this component should render as. Can be overwrite by |
asChild | boolean Change the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details. | |
level* | number Level of depth | |
value* | Record<string, any> Value given to this item |
Emit | Payload |
---|---|
select | [event: SelectEvent<Record<string, any>>] Event handler called when the selecting item. |
toggle | [event: ToggleEvent<Record<string, any>>] Event handler called when the selecting item. |
Slots (default) | Payload |
---|---|
isExpanded | boolean |
isSelected | boolean |
isIndeterminate | boolean | undefined |
handleToggle |
|
handleSelect |
|
Data Attribute | Value |
---|---|
[data-indent] | Number |
[data-expanded] | Present when expanded |
[data-selected] | Present when selected |
Virtualizer
Virtual container to achieve list virtualization.
Prop | Default | Type |
---|---|---|
estimateSize | number Estimated size (in px) of each item | |
textContent | ((item: Record<string, any>) => string) text content for each item to achieve type-ahead feature |
Slots (default) | Payload |
---|---|
item | FlattenedItem<Record<string, any>> |
virtualizer | Virtualizer<Element | Window, Element> |
virtualItem | VirtualItem<Element> |
Examples
Selecting multiple items
The Tree
component allows you to select multiple items. You can enable this by providing an array of values instead of a single value.
<script setup lang="ts">
import { ref } from 'vue'
import { TreeRoot } from 'radix-vue'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
const selectedPeople = ref([people[0], people[1]])
</script>
<template>
<TreeRoot
v-model="selectedPeople"
multiple
>
...
</TreeRoot>
</template>
Virtual List
Rendering a long list of item can slow down the app, thus using virtualization would significantly improve the performance.
<script setup lang="ts">
import { ref } from 'vue'
import { TreeItem, TreeRoot, TreeVirtualizer } from 'radix-vue'
</script>
<template>
<TreeRoot :items>
<!-- checkout https://radix-vue.com/components/tree.html#virtualizer -->
<TreeVirtualizer
v-slot="{ item }"
:text-content="(opt) => opt.name"
>
<TreeItem v-bind="item.bind">
{{ person.name }}
</TreeItem>
</TreeVirtualizer>
</TreeRoot>
</template>
With Checkbox
Some Tree
component might want to show toggled/indeterminate
checkbox. We can change the behavior of the Tree
component by using a few props and preventDefault
event.
We set propagateSelect
to true
because we want the parent checkbox to select/deselect it's descendants. Then, we add a checkbox that triggers select
event.
<script setup lang="ts">
import { ref } from 'vue'
import { TreeItem, TreeRoot } from 'radix-vue'
</script>
<template>
<TreeRoot
v-slot="{ flattenItems }"
:items
multiple
propagate-select
>
<TreeItem
v-for="item in flattenItems"
:key="item._id"
v-bind="item.bind"
v-slot="{ handleSelect, isSelected, isIndeterminate }"
@select="(event) => {
if (event.detail.originalEvent.type === 'click')
event.preventDefault()
}"
@toggle="(event) => {
if (event.detail.originalEvent.type === 'keydown')
event.preventDefault()
}"
>
<Icon
v-if="item.hasChildren"
icon="radix-icons:chevron-down"
/>
<button
tabindex="-1"
@click.stop
@change="handleSelect"
>
<Icon
v-if="isSelected"
icon="radix-icons:check"
/>
<Icon
v-else-if="isIndeterminate"
icon="radix-icons:dash"
/>
<Icon
v-else
icon="radix-icons:box"
/>
</button>
<div class="pl-2">
{{ item.value.title }}
</div>
</TreeItem>
</TreeRoot>
</template>
Nested Tree Node
The default example shows flatten tree items and nodes, this enables Virtualization and custom feature such as Drag & Drop easier. However, you can also build it to have nested DOM node.
In Tree.vue
,
<script setup lang="ts">
import { TreeItem } from 'radix-vue'
interface TreeNode {
title: string
icon: string
children?: TreeNode[]
}
withDefaults(defineProps<{
treeItems: TreeNode[]
level?: number
}>(), { level: 0 })
</script>
<template>
<li
v-for=" tree in treeItems"
:key="tree.title"
>
<TreeItem
v-slot="{ isExpanded }"
as-child
:level="level"
:value="tree"
>
<button>…</button>
<ul v-if="isExpanded && tree.children">
<Tree
:tree-items="tree.children"
:level="level + 1"
/>
</ul>
</TreeItem>
</li>
</template>
In CustomTree.vue
<template>
<TreeRoot
:items="items"
:get-key="(item) => item.title"
>
<Tree :tree-items="items" />
</TreeRoot>
</template>
Custom children schema
By default, <TreeRoot />
expects you to provide the list of node's children by passing a list of children
for every node. You can override that by providing the getChildren
prop.
::: NOTE If the node doesn't have any children, getChildren
should return undefined
instead of an empty array. :::
<script setup lang="ts">
import { ref } from 'vue'
import { TreeRoot } from 'radix-vue'
interface FileNode {
title: string
icon: string
}
interface DirectoryNode {
title: string
icon: string
directories?: DirectoryNode[]
files?: FileNode[]
}
</script>
<template>
<TreeRoot
:items="items"
:get-key="(item) => item.title"
:get-children="(item) => (!item.files) ? item.directories : (!item.directories) ? item.files : [...item.directories, ...item.files]"
>
...
</TreeRoot>
</template>
Draggable/Sortable Tree
For more complex draggable Tree
component, in this example we will be using pragmatic-drag-and-drop, as the core package for handling dnd.
Accessibility
Adheres to the Tree WAI-ARIA design pattern.
Keyboard Interactions
Key | Description |
---|---|
Enter | When highlight on TreeItem , selects the focused item. |
ArrowDown | When focus is on TreeItem , moves focus to the next item. |
ArrowUp | When focus is on TreeItem , moves focus to the previous item. |
ArrowRight | When focus is on a closed TreeItem (node), it opens the node without moving focus. When on an open node, it moves focus to the first child node. When on an end node, it does nothing. |
ArrowLeft | When focus is on an open TreeItem (node), closes the node. When focus is on a child node that is also either an end node or a closed node, moves focus to its parent node. When focus is on a root node that is also either an end node or a closed node, does nothing. |
HomePageUp | Moves focus first TreeItem |
EndPageDown | Moves focus last TreeItem |