Tree

PohonGitHub
A tree view component to display and interact with hierarchical data structures.

    Directory Structure

  • components
  • app.vue
  • nuxt.config.ts
<script setup lang="ts">
import { ATreeItem, ATreeRoot } from 'akar';

const items = [
  {
    title: 'composables',
    icon: 'lucide:folder',
    children: [
      { title: 'useAuth.ts', icon: 'i-vscode-icons:file-type-typescript' },
      { title: 'useUser.ts', icon: 'i-vscode-icons:file-type-typescript' },
    ],
  },
  {
    title: 'components',
    icon: 'lucide:folder',
    children: [
      {
        title: 'Home',
        icon: 'lucide:folder',
        children: [
          { title: 'Card.vue', icon: 'i-vscode-icons:file-type-vue' },
          { title: 'Button.vue', icon: 'i-vscode-icons:file-type-vue' },
        ],
      },
    ],
  },
  { title: 'app.vue', icon: 'i-vscode-icons:file-type-vue' },
  { title: 'nuxt.config.ts', icon: 'i-vscode-icons:file-type-nuxt' },
];
</script>

<template>
  <ATreeRoot
    v-slot="{ flattenItems }"
    class="text-sm color-text-muted font-medium p-2 list-none border rounded-lg bg-background w-56 select-none shadow-sm"
    :items="items"
    :get-key="(item) => item.title"
    :default-expanded="['components']"
  >
    <h2 class="text-sm font-semibold px-2 pb-3 pt-1">
      Directory Structure
    </h2>
    <ATreeItem
      v-for="item in flattenItems"
      v-slot="{ isExpanded }"
      :key="item._id"
      :style="{ 'padding-left': `${item.level - 0.5}rem` }"
      v-bind="item.bind"
      class="data-[selected]:accented my-0.5 px-2 py-1 outline-none rounded flex items-center data-[selected]:(color-primary bg-background-accented) focus:ring-2 focus:ring-primary"
    >
      <template v-if="item.hasChildren">
        <i
          v-if="!isExpanded"
          class="i-lucide:folder size-4"
        />
        <i
          v-else
          class="i-lucide:folder-open size-4"
        />
      </template>
      <i
        v-else
        class="size-4"
        :class="item.value.icon || 'i-lucide:file'"
      />
      <div class="pl-2">
        {{ item.value.title }}
      </div>
    </ATreeItem>
  </ATreeRoot>
</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.

Anatomy

Import all parts and piece them together.

<script setup>
import { ATreeItem, ATreeRoot, ATreeVirtualizer } from 'akar';
</script>

<template>
  <ATreeRoot>
    <ATreeItem />

    <!-- or with virtual -->
    <ATreeVirtualizer>
      <ATreeItem />
    </ATreeVirtualizer>
  </ATreeRoot>
</template>

Pohon

One benefit of using Akar is its flexibility and low-level control over the components. However, this also means that you may need to manually construct more complex UI elements by combining multiple Akar components together.

If you feel there's a lot of elements that needs to be constructed manually using Akar, consider using Pohon UI instead. It provides a higher-level abstraction over Akar components with pre-defined styles and behaviors that can help you build UIs faster.

API Reference

Root

Contains all the parts of a tree.

Props

Prop Default Type
as'ul'APrimitiveAsTag | Component

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

asChildboolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

bubbleSelectboolean

When true, selecting children will update the parent state.

defaultExpandedstring[]

The value of the expanded tree when initially rendered. Use when you do not need to control the state of the expanded tree

defaultValueRecord<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 AConfigProvider or assumes LTR (left-to-right) reading mode.

disabledboolean

When true, prevents the user from interacting with tree

expandedstring[]

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

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

itemsRecord<string, any>[]

List of items

modelValueRecord<string, any> | Record<string, any>[]

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

multipleboolean

Whether multiple options can be selected or not.

propagateSelectboolean

When true, selecting parent will select the descendants.

selectionBehavior'toggle''replace' | 'toggle'

How multiple selection should behave in the collection.

Emits

Event Type
update:expanded[val: string[]]
update:modelValue[val: Record<string, any> | Record<string, any>[]]

Event handler called when the value changes.

Slots

Slot Type
flattenItemsFlattenedItem<Record<string, any>>[]
modelValueRecord<string, any> | Record<string, any>[]
expandedstring[]

Item

The item component.

Props

Prop Default Type
as'li'APrimitiveAsTag | Component

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

asChildboolean

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

Emits

Event Type
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

Slot Type
isExpandedboolean
isSelectedboolean
isIndeterminateboolean
handleToggle(): void
handleSelect(): void

Data Attributes

Attribute Value
[data-indent]Number
[data-expanded]Present when expanded
[data-selected]Present when selected

Virtualizer

Virtual container to achieve list virtualization.

Props

Prop Default Type
estimateSizenumber

Estimated size (in px) of each item

overscannumber

Number of items rendered outside the visible area

textContent((item: Record<string, any>) => string)

Text content for each item to achieve type-ahead feature

Slots

Slot Type
itemFlattenedItem<Record<string, any>>
virtualizerVirtualizer<Element | Window, Element>
virtualItemVirtualItem

Examples

Selecting multiple items

The ATree component allows you to select multiple items. You can enable this by providing an array of values instead of a single value and set multiple="true".

<script setup lang="ts">
import { ATreeRoot } from 'akar';
import { ref } from '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>
  <ATreeRoot
    v-model="selectedPeople"
    multiple
  >
    ...
  </ATreeRoot>
</template>

Virtual List

Rendering a long list of item can slow down the app, thus using virtualization would significantly improve the performance.

See the virtualization guide for more general info on virtualization.

<script setup lang="ts">
import { ATreeItem, ATreeRoot, ATreeVirtualizer } from 'akar';
import { ref } from 'vue';
</script>

<template>
  <ATreeRoot :items>
    <ATreeVirtualizer
      v-slot="{ item }"
      :text-content="(opt) => opt.name"
    >
      <ATreeItem v-bind="item.bind">
        {{ person.name }}
      </ATreeItem>
    </ATreeVirtualizer>
  </ATreeRoot>
</template>

With Checkbox

Some ATree component might want to show toggled/indeterminate checkbox. We can change the behavior of the ATree 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 { ATreeItem, ATreeRoot } from 'akar';
import { ref } from 'vue';
</script>

<template>
  <ATreeRoot
    v-slot="{ flattenItems }"
    :items
    multiple
    propagate-select
  >
    <ATreeItem
      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>
    </ATreeItem>
  </ATreeRoot>
</template>

Nested ATree 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 ATree.vue,

<script setup lang="ts">
import { ATreeItem } from 'akar';

interface ATreeNode {
  title: string;
  icon: string;
  children?: Array<ATreeNode>;
}

withDefaults(defineProps<{
  treeItems: Array<ATreeNode>;
  level?: number;
}>(), { level: 0 });
</script>

<template>
  <li
    v-for=" tree in treeItems"
    :key="tree.title"
  >
    <ATreeItem
      v-slot="{ isExpanded }"
      as-child
      :level="level"
      :value="tree"
    >
      <button>…</button>

      <ul v-if="isExpanded && tree.children">
        <ATree
          :tree-items="tree.children"
          :level="level + 1"
        />
      </ul>
    </ATreeItem>
  </li>
</template>

In CustomATree.vue

<template>
  <ATreeRoot
    :items="items"
    :get-key="(item) => item.title"
  >
    <ATree :tree-items="items" />
  </ATreeRoot>
</template>

Custom children schema

By default, <ATreeRoot /> 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.

If the node doesn't have any children, getChildren should return undefined instead of an empty array.
<script setup lang="ts">
import { ATreeRoot } from 'akar';
import { ref } from 'vue';

interface FileNode {
  title: string;
  icon: string;
}

interface DirectoryNode {
  title: string;
  icon: string;
  directories?: Array<DirectoryNode>;
  files?: Array<FileNode>;
}
</script>

<template>
  <ATreeRoot
    :items="items"
    :get-key="(item) => item.title"
    :get-children="(item) => (!item.files) ? item.directories : (!item.directories) ? item.files : [...item.directories, ...item.files]"
  >
    ...
  </ATreeRoot>
</template>

Draggable/Sortable ATree

For more complex draggable ATree component, in this example we will be using pragmatic-drag-and-drop, as the core package for handling dnd.

Accessibility

Adheres to the ATree WAI-ARIA design pattern.

Keyboard Interactions

Key Description
EnterSpace

When highlight on ATreeItem, selects the focused item.

ArrowDown

When focus is on ATreeItem, moves focus to the next item.

ArrowUp

When focus is on ATreeItem, moves focus to the previous item.

ArrowRight

When focus is on a closed ATreeItem (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 ATreeItem (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 ATreeItem

EndPageDown

Moves focus last ATreeItem