Skip to content

Tree

Alpha
A tree view widget displays a hierarchical list of items that can be expanded or collapsed to show or hide their child items, such as in a file system navigator.

    Directory Structure

  • components
  • app.vue
  • nuxt.config.ts
vue
<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.

sh
$ npm add radix-vue

Anatomy

Import all parts and piece them together.

vue
<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.

PropDefaultType
as
'ul'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

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.
If omitted, inherits globally from ConfigProvider or assumes LTR (left-to-right) reading mode.

disabled
boolean

When true, prevents the user from interacting with tree

expanded
string[]

The controlled value of the expanded item. Can be binded-with with v-model.

getKey*
(val: Record<string, any>) => string

This function is passed the index of each item and should return a unique key for that item

getChildren
(val: Record<string, any>) => Record<string, any>[] | undefined

This function is passed the index of each item and should return a list of children 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 v-model.

multiple
boolean

Whether multiple options can be selected or not.

propagateSelect
boolean

When true, selecting parent will select the descendants.

selectionBehavior
'toggle'
'replace' | 'toggle'

How multiple selection should behave in the collection.

EmitPayload
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.

PropDefaultType
as
'li'
AsTag | Component

The element or component this component should render as. Can be overwrite by asChild

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

EmitPayload
select
[event: SelectEvent<Record<string, any>>]

Event handler called when the selecting item.
It can be prevented by calling event.preventDefault.

toggle
[event: ToggleEvent<Record<string, any>>]

Event handler called when the selecting item.
It can be prevented by calling event.preventDefault.

Slots (default)Payload
isExpanded
boolean
isSelected
boolean
isIndeterminate
boolean | undefined
handleToggle
handleSelect
Data AttributeValue
[data-indent]Number
[data-expanded]Present when expanded
[data-selected]Present when selected

Virtualizer

Virtual container to achieve list virtualization.

PropDefaultType
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>>

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.

vue
<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.

vue
<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.

vue
<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,

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

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. :::

vue
<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.

Stackblitz Demo

Accessibility

Adheres to the Tree WAI-ARIA design pattern.

Keyboard Interactions

KeyDescription
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