<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>
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>
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.
Contains all the parts of a tree.
| Prop | Default | Type |
|---|---|---|
as | 'ul' | APrimitiveAsTag | ComponentThe element or component this component should render as. Can be overwritten by |
asChild | booleanChange the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details. | |
bubbleSelect | booleanWhen | |
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 | booleanWhen | |
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>): stringThis 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 | booleanWhether multiple options can be selected or not. | |
propagateSelect | booleanWhen | |
selectionBehavior | 'toggle' | 'replace' | 'toggle'How multiple selection should behave in the collection. |
| Event | Type |
|---|---|
update:expanded | [val: string[]] |
update:modelValue | [val: Record<string, any> | Record<string, any>[]]Event handler called when the value changes. |
| Slot | Type |
|---|---|
flattenItems | FlattenedItem<Record<string, any>>[] |
modelValue | Record<string, any> | Record<string, any>[] |
expanded | string[] |
The item component.
| Prop | Default | Type |
|---|---|---|
as | 'li' | APrimitiveAsTag | ComponentThe element or component this component should render as. Can be overwritten by |
asChild | booleanChange the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details. | |
level* | numberLevel of depth | |
value* | Record<string, any>Value given to this item |
| Event | Type |
|---|---|
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. |
| Slot | Type |
|---|---|
isExpanded | boolean |
isSelected | boolean |
isIndeterminate | boolean |
handleToggle | (): void |
handleSelect | (): void |
| Attribute | Value |
|---|---|
[data-indent] | Number |
[data-expanded] | Present when expanded |
[data-selected] | Present when selected |
Virtual container to achieve list virtualization.
| Prop | Default | Type |
|---|---|---|
estimateSize | numberEstimated size (in px) of each item | |
overscan | numberNumber of items rendered outside the visible area | |
textContent | ((item: Record<string, any>) => string)Text content for each item to achieve type-ahead feature |
| Slot | Type |
|---|---|
item | FlattenedItem<Record<string, any>> |
virtualizer | Virtualizer<Element | Window, Element> |
virtualItem | VirtualItem |
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>
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>
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>
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>
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.
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>
For more complex draggable ATree component, in this example we will be using pragmatic-drag-and-drop, as the core package for handling dnd.
Adheres to the ATree WAI-ARIA design pattern.
| Key | Description |
|---|---|
EnterSpace | When highlight on |
ArrowDown | When focus is on |
ArrowUp | When focus is on |
ArrowRight | When focus is on a closed |
ArrowLeft | When focus is on an open |
HomePageUp | Moves focus first |
EndPageDown | Moves focus last |