Use the Tree component to display a hierarchical structure of items.
<script setup lang="ts">
const items = ref([
{
label: 'app/',
defaultExpanded: true,
children: [
{
label: 'composables/',
children: [
{
label: 'useAuth.ts',
icon: 'i-vscode-icons:file-type-typescript'
},
{
label: 'useUser.ts',
icon: 'i-vscode-icons:file-type-typescript'
}
]
},
{
label: 'components/',
defaultExpanded: true,
children: [
{
label: 'Card.vue',
icon: 'i-vscode-icons:file-type-vue'
},
{
label: 'Button.vue',
icon: 'i-vscode-icons:file-type-vue'
}
]
}
]
},
{
label: 'app.vue',
icon: 'i-vscode-icons:file-type-vue'
},
{
label: 'nuxt.config.ts',
icon: 'i-vscode-icons:file-type-nuxt'
}
])
</script>
<template>
<PTree :items="items" />
</template>
Use the items prop as an array of objects with the following properties:
icon?: stringlabel?: stringtrailingIcon?: stringdefaultExpanded?: booleandisabled?: booleanslot?: stringchildren?: TreeItem[]onToggle?: (e: TreeItemToggleEvent<TreeItem>) => voidonSelect?: (e: TreeItemSelectEvent<TreeItem>) => voidclass?: anypohon?: { item?: ClassNameValue, itemWithChildren?: ClassNameValue, link?: ClassNameValue, linkLeadingIcon?: ClassNameValue, linkLabel?: ClassNameValue, linkTrailing?: ClassNameValue, linkTrailingIcon?: ClassNameValue, listWithChildren?: ClassNameValue }label prop as identifier if no get-key is provided. Ideally you should provide a get-key function prop to return a unique identifier. Alternatively, you can use the labelKey prop to specify which property to use as the unique identifier.<script setup lang="ts">
import type { TreeItem } from 'pohon-ui'
const items = ref<TreeItem[]>([
{
label: 'app/',
defaultExpanded: true,
children: [
{
label: 'composables/',
children: [
{
label: 'useAuth.ts',
icon: 'i-vscode-icons:file-type-typescript'
},
{
label: 'useUser.ts',
icon: 'i-vscode-icons:file-type-typescript'
}
]
},
{
label: 'components/',
defaultExpanded: true,
children: [
{
label: 'Card.vue',
icon: 'i-vscode-icons:file-type-vue'
},
{
label: 'Button.vue',
icon: 'i-vscode-icons:file-type-vue'
}
]
}
]
},
{
label: 'app.vue',
icon: 'i-vscode-icons:file-type-vue'
},
{
label: 'nuxt.config.ts',
icon: 'i-vscode-icons:file-type-nuxt'
}
])
</script>
<template>
<PTree :items="items" />
</template>
Use the multiple prop to allow multiple item selections.
<script setup lang="ts">
import type { TreeItem } from 'pohon-ui'
const items = ref<TreeItem[]>([
{
label: 'app/',
defaultExpanded: true,
children: [
{
label: 'composables/',
children: [
{
label: 'useAuth.ts',
icon: 'i-vscode-icons:file-type-typescript'
},
{
label: 'useUser.ts',
icon: 'i-vscode-icons:file-type-typescript'
}
]
},
{
label: 'components/',
defaultExpanded: true,
children: [
{
label: 'Card.vue',
icon: 'i-vscode-icons:file-type-vue'
},
{
label: 'Button.vue',
icon: 'i-vscode-icons:file-type-vue'
}
]
}
]
},
{
label: 'app.vue',
icon: 'i-vscode-icons:file-type-vue'
},
{
label: 'nuxt.config.ts',
icon: 'i-vscode-icons:file-type-nuxt'
}
])
</script>
<template>
<PTree multiple :items="items" />
</template>
Use the nested prop to control whether the Tree is rendered with nested structure or as a flat list. Defaults to true.
<script setup lang="ts">
import type { TreeItem } from 'pohon-ui'
const items = ref<TreeItem[]>([
{
label: 'app/',
defaultExpanded: true,
children: [
{
label: 'composables/',
children: [
{
label: 'useAuth.ts',
icon: 'i-vscode-icons:file-type-typescript'
},
{
label: 'useUser.ts',
icon: 'i-vscode-icons:file-type-typescript'
}
]
},
{
label: 'components/',
defaultExpanded: true,
children: [
{
label: 'Card.vue',
icon: 'i-vscode-icons:file-type-vue'
},
{
label: 'Button.vue',
icon: 'i-vscode-icons:file-type-vue'
}
]
}
]
},
{
label: 'app.vue',
icon: 'i-vscode-icons:file-type-vue'
},
{
label: 'nuxt.config.ts',
icon: 'i-vscode-icons:file-type-nuxt'
}
])
</script>
<template>
<PTree :items="items" />
</template>
nested is false, all items are rendered at the same level with indentation to indicate hierarchy. This is useful for virtualization or drag and drop functionality.Use the color prop to change the color of the Tree.
<script setup lang="ts">
import type { TreeItem } from 'pohon-ui'
const items = ref<TreeItem[]>([
{
label: 'app/',
defaultExpanded: true,
children: [
{
label: 'composables/',
children: [
{
label: 'useAuth.ts',
icon: 'i-vscode-icons:file-type-typescript'
},
{
label: 'useUser.ts',
icon: 'i-vscode-icons:file-type-typescript'
}
]
},
{
label: 'components/',
defaultExpanded: true,
children: [
{
label: 'Card.vue',
icon: 'i-vscode-icons:file-type-vue'
},
{
label: 'Button.vue',
icon: 'i-vscode-icons:file-type-vue'
}
]
}
]
},
{
label: 'app.vue',
icon: 'i-vscode-icons:file-type-vue'
},
{
label: 'nuxt.config.ts',
icon: 'i-vscode-icons:file-type-nuxt'
}
])
</script>
<template>
<PTree color="neutral" :items="items" />
</template>
Use the size prop to change the size of the Tree.
<script setup lang="ts">
import type { TreeItem } from 'pohon-ui'
const items = ref<TreeItem[]>([
{
label: 'app/',
defaultExpanded: true,
children: [
{
label: 'composables/',
children: [
{
label: 'useAuth.ts',
icon: 'i-vscode-icons:file-type-typescript'
},
{
label: 'useUser.ts',
icon: 'i-vscode-icons:file-type-typescript'
}
]
},
{
label: 'components/',
defaultExpanded: true,
children: [
{
label: 'Card.vue',
icon: 'i-vscode-icons:file-type-vue'
},
{
label: 'Button.vue',
icon: 'i-vscode-icons:file-type-vue'
}
]
}
]
},
{
label: 'app.vue',
icon: 'i-vscode-icons:file-type-vue'
},
{
label: 'nuxt.config.ts',
icon: 'i-vscode-icons:file-type-nuxt'
}
])
</script>
<template>
<PTree size="xl" :items="items" />
</template>
Use the trailing-icon prop to customize the trailing Icon of a parent node. Defaults to i-lucide:chevron-down.
<script setup lang="ts">
import type { TreeItem } from 'pohon-ui'
const items = ref<TreeItem[]>([
{
label: 'app/',
defaultExpanded: true,
children: [
{
label: 'composables/',
trailingIcon: 'i-lucide:chevron-down',
children: [
{
label: 'useAuth.ts',
icon: 'i-vscode-icons:file-type-typescript'
},
{
label: 'useUser.ts',
icon: 'i-vscode-icons:file-type-typescript'
}
]
},
{
label: 'components/',
defaultExpanded: true,
children: [
{
label: 'Card.vue',
icon: 'i-vscode-icons:file-type-vue'
},
{
label: 'Button.vue',
icon: 'i-vscode-icons:file-type-vue'
}
]
}
]
},
{
label: 'app.vue',
icon: 'i-vscode-icons:file-type-vue'
},
{
label: 'nuxt.config.ts',
icon: 'i-vscode-icons:file-type-nuxt'
}
])
</script>
<template>
<PTree trailing-icon="i-lucide:arrow-down" :items="items" />
</template>
Use the expanded-icon and collapsed-icon props to customize the icons of a parent node when it is expanded or collapsed. Defaults to i-lucide:folder-open and i-lucide:folder respectively.
<script setup lang="ts">
import type { TreeItem } from 'pohon-ui'
const items = ref<TreeItem[]>([
{
label: 'app/',
defaultExpanded: true,
children: [
{
label: 'composables/',
children: [
{
label: 'useAuth.ts',
icon: 'i-vscode-icons:file-type-typescript'
},
{
label: 'useUser.ts',
icon: 'i-vscode-icons:file-type-typescript'
}
]
},
{
label: 'components/',
defaultExpanded: true,
children: [
{
label: 'Card.vue',
icon: 'i-vscode-icons:file-type-vue'
},
{
label: 'Button.vue',
icon: 'i-vscode-icons:file-type-vue'
}
]
}
]
},
{
label: 'app.vue',
icon: 'i-vscode-icons:file-type-vue'
},
{
label: 'nuxt.config.ts',
icon: 'i-vscode-icons:file-type-nuxt'
}
])
</script>
<template>
<PTree expanded-icon="i-lucide:book-open" collapsed-icon="i-lucide:book" :items="items" />
</template>
app.config.ts under pohon.icons.folder and pohon.icons.folderOpen keys.vite.config.ts under pohon.icons.folder and pohon.icons.folderOpen keys.Use the disabled prop to prevent any user interaction with the Tree.
<script setup lang="ts">
import type { TreeItem } from 'pohon-ui'
const items = ref<TreeItem[]>([
{
label: 'app',
icon: 'i-lucide:folder',
defaultExpanded: true,
children: [
{
label: 'composables',
icon: 'i-lucide:folder',
children: [
{
label: 'useAuth.ts',
icon: 'i-vscode-icons:file-type-typescript'
},
{
label: 'useUser.ts',
icon: 'i-vscode-icons:file-type-typescript'
}
]
},
{
label: 'components',
icon: 'i-lucide:folder',
children: [
{
label: 'Home',
icon: 'i-lucide:folder',
children: [
{
label: 'Card.vue',
icon: 'i-vscode-icons:file-type-vue'
},
{
label: 'Button.vue',
icon: 'i-vscode-icons:file-type-vue'
}
]
}
]
}
]
},
{
label: 'app.vue',
icon: 'i-vscode-icons:file-type-vue'
},
{
label: 'nuxt.config.ts',
icon: 'i-vscode-icons:file-type-nuxt'
}
])
</script>
<template>
<PTree disabled :items="items" />
</template>
item.disabled.You can control the selected item(s) by using the default-value prop or the v-model directive.
<script setup lang="ts">
import type { PTreeItem } from 'pohon-ui';
import { ref } from 'vue';
const items: Array<PTreeItem> = [
{
label: 'app/',
defaultExpanded: true,
children: [
{
label: 'composables/',
children: [
{ label: 'useAuth.ts', icon: 'i-vscode-icons:file-type-typescript' },
{ label: 'useUser.ts', icon: 'i-vscode-icons:file-type-typescript' },
],
},
{
label: 'components/',
defaultExpanded: true,
children: [
{ label: 'Card.vue', icon: 'i-vscode-icons:file-type-vue' },
{ label: 'Button.vue', icon: 'i-vscode-icons:file-type-vue' },
],
},
],
},
{ label: 'app.vue', icon: 'i-vscode-icons:file-type-vue' },
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons:file-type-nuxt' },
];
const value = ref();
</script>
<template>
<PTree
v-model="value"
:items="items"
/>
</template>
If you want to prevent an item from being selected, you can use the item.onSelect() property or the global select event:
<script setup lang="ts">
import type { ATreeItemSelectEvent } from 'akar';
import type { PTreeItem } from 'pohon-ui';
const items: Array<PTreeItem> = [
{
label: 'app/',
defaultExpanded: true,
onSelect: (e: Event) => {
e.preventDefault();
},
children: [
{
label: 'composables/',
children: [
{ label: 'useAuth.ts', icon: 'i-vscode-icons:file-type-typescript' },
{ label: 'useUser.ts', icon: 'i-vscode-icons:file-type-typescript' },
],
},
{
label: 'components/',
defaultExpanded: true,
children: [
{ label: 'Card.vue', icon: 'i-vscode-icons:file-type-vue' },
{ label: 'Button.vue', icon: 'i-vscode-icons:file-type-vue' },
],
},
],
},
{ label: 'app.vue', icon: 'i-vscode-icons:file-type-vue' },
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons:file-type-nuxt' },
];
function onSelect(e: ATreeItemSelectEvent<PTreeItem>) {
if (e.detail.originalEvent.type === 'click') {
e.preventDefault();
}
}
</script>
<template>
<PTree
:items="items"
@select="onSelect"
/>
</template>
You can control the expanded items by using the default-expanded prop or the v-model directive.
<script setup lang="ts">
import type { PTreeItem } from 'pohon-ui';
import { ref } from 'vue';
const items = [
{
label: 'app/',
id: 'app',
children: [
{
label: 'composables/',
id: 'app/composables',
children: [
{ label: 'useAuth.ts', icon: 'i-vscode-icons:file-type-typescript' },
{ label: 'useUser.ts', icon: 'i-vscode-icons:file-type-typescript' },
],
},
{
label: 'components/',
id: 'app/components',
children: [
{ label: 'Card.vue', icon: 'i-vscode-icons:file-type-vue' },
{ label: 'Button.vue', icon: 'i-vscode-icons:file-type-vue' },
],
},
],
},
{ label: 'app.vue', id: 'app.vue', icon: 'i-vscode-icons:file-type-vue' },
{ label: 'nuxt.config.ts', id: 'nuxt.config.ts', icon: 'i-vscode-icons:file-type-nuxt' },
] satisfies Array<PTreeItem>;
const expanded = ref(['app', 'app/composables']);
</script>
<template>
<PTree
v-model:expanded="expanded"
:items="items"
:get-key="i => i.id"
/>
</template>
If you want to prevent an item from being expanded, you can use the item.onToggle() property or the global toggle event:
<script setup lang="ts">
import type { ATreeItemToggleEvent } from 'akar';
import type { PTreeItem } from 'pohon-ui';
const items: Array<PTreeItem> = [
{
label: 'app/',
defaultExpanded: true,
onToggle: (e: Event) => {
e.preventDefault();
},
children: [
{
label: 'composables/',
children: [
{ label: 'useAuth.ts', icon: 'i-vscode-icons:file-type-typescript' },
{ label: 'useUser.ts', icon: 'i-vscode-icons:file-type-typescript' },
],
},
{
label: 'components/',
defaultExpanded: true,
children: [
{ label: 'Card.vue', icon: 'i-vscode-icons:file-type-vue' },
{ label: 'Button.vue', icon: 'i-vscode-icons:file-type-vue' },
],
},
],
},
{ label: 'app.vue', icon: 'i-vscode-icons:file-type-vue' },
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons:file-type-nuxt' },
];
function onToggle(e: ATreeItemToggleEvent<PTreeItem>) {
if (e.detail.originalEvent.type === 'keydown') {
e.preventDefault();
}
}
</script>
<template>
<PTree
:items="items"
@toggle="onToggle"
/>
</template>
You can use the item-leading slot to add a Checkbox to the items. Use the multiple, propagate-select and bubble-select props to enable multi-selection with parent-child relationship and the select and toggle events to control the selected and expanded state of the items.
<script setup lang="ts">
import type { ATreeItemSelectEvent } from 'akar';
import type { PTreeItem } from 'pohon-ui';
import { ref } from 'vue';
const items: Array<PTreeItem> = [
{
label: 'app/',
defaultExpanded: true,
children: [
{
label: 'composables/',
children: [
{ label: 'useAuth.ts' },
{ label: 'useUser.ts' },
],
},
{
label: 'components/',
defaultExpanded: true,
children: [
{ label: 'Card.vue' },
{ label: 'Button.vue' },
],
},
],
},
{ label: 'app.vue' },
{ label: 'nuxt.config.ts' },
];
const value = ref<(typeof items)>([]);
function onSelect(e: ATreeItemSelectEvent<PTreeItem>) {
if (e.detail.originalEvent.type === 'click') {
e.preventDefault();
}
}
</script>
<template>
<PTree
v-model="value"
:as="{ link: 'div' }"
:items="items"
multiple
propagate-select
bubble-select
@select="onSelect"
>
<template #item-leading="{ selected, indeterminate, handleSelect }">
<PCheckbox
:model-value="indeterminate ? 'indeterminate' : selected"
tabindex="-1"
@change="handleSelect"
@click.stop
/>
</template>
</PTree>
</template>
as prop to change the items from button to div as the Checkbox is also rendered as a button.Use the useSortable composable from @vueuse/integrations to enable drag and drop functionality on the Tree. This integration wraps Sortable.js to provide a seamless drag and drop experience.
<script setup lang="ts">
import type { PTreeItem } from 'pohon-ui'
import { useSortable } from '@vueuse/integrations/useSortable'
import { shallowRef, useTemplateRef } from 'vue'
const items = shallowRef<Array<PTreeItem>>([
{
label: 'app/',
defaultExpanded: true,
children: [
{
label: 'composables/',
children: [
{ label: 'useAuth.ts', icon: 'i-vscode-icons:file-type-typescript' },
{ label: 'useUser.ts', icon: 'i-vscode-icons:file-type-typescript' }
]
},
{
label: 'components/',
defaultExpanded: true,
children: [
{ label: 'Card.vue', icon: 'i-vscode-icons:file-type-vue' },
{ label: 'Button.vue', icon: 'i-vscode-icons:file-type-vue' }
]
}
]
},
{ label: 'app.vue', icon: 'i-vscode-icons:file-type-vue' },
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons:file-type-nuxt' }
])
function flatten(
items: Array<PTreeItem>,
parent = items
): Array<{ item: PTreeItem; parent: Array<PTreeItem>; index: number }> {
return items.flatMap((item, index) => [
{ item, parent, index },
...(item.children?.length && item.defaultExpanded ? flatten(item.children, item.children) : [])
])
}
function moveItem(oldIndex: number, newIndex: number) {
if (oldIndex === newIndex) {
return
}
const flat = flatten(items.value)
const source = flat[oldIndex]
const target = flat[newIndex]
if (!source || !target) {
return
}
const [moved] = source.parent.splice(source.index, 1)
if (!moved) {
return
}
const updatedFlat = flatten(items.value)
const updatedTarget = updatedFlat.find(({ item }) => item === target.item)
if (!updatedTarget) {
return
}
const insertIndex = oldIndex < newIndex ? updatedTarget.index + 1 : updatedTarget.index
updatedTarget.parent.splice(insertIndex, 0, moved)
}
const tree = useTemplateRef<HTMLElement>('tree')
useSortable(tree, items, {
animation: 150,
ghostClass: 'opacity-50',
onUpdate: (e: any) => moveItem(e.oldIndex, e.newIndex)
})
</script>
<template>
<PTree ref="tree" :nested="false" :unmount-on-hide="false" :items="items" />
</template>
nested prop to false to have a flat list of items so that the items can be dragged and dropped.Use the virtualize prop to enable virtualization for large lists as a boolean or an object with options like { estimateSize: 32, overscan: 12 }.
nested prop to false.<script setup lang="ts">
import type { PTreeItem } from 'pohon-ui'
const items: Array<PTreeItem> = Array(1000)
.fill(0)
.map((_, i) => ({
label: `Item ${i + 1}`,
children: [
{ label: `Child ${i + 1}-1`, icon: 'i-lucide:file' },
{ label: `Child ${i + 1}-2`, icon: 'i-lucide:file' }
]
}))
</script>
<template>
<PTree virtualize :items="items" class="h-80" />
</template>
Use the slot property to customize a specific item.
You will have access to the following slots:
#{{ item.slot }}-wrapper#{{ item.slot }}#{{ item.slot }}-leading#{{ item.slot }}-label#{{ item.slot }}-trailing<script setup lang="ts">
import type { PTreeItem } from 'pohon-ui';
const items = [
{
label: 'app/',
slot: 'app' as const,
defaultExpanded: true,
children: [
{
label: 'composables/',
children: [
{ label: 'useAuth.ts', icon: 'i-vscode-icons:file-type-typescript' },
{ label: 'useUser.ts', icon: 'i-vscode-icons:file-type-typescript' },
],
},
{
label: 'components/',
defaultExpanded: true,
children: [
{ label: 'Card.vue', icon: 'i-vscode-icons:file-type-vue' },
{ label: 'Button.vue', icon: 'i-vscode-icons:file-type-vue' },
],
},
],
},
{ label: 'app.vue', icon: 'i-vscode-icons:file-type-vue' },
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons:file-type-nuxt' },
] satisfies Array<PTreeItem>;
</script>
<template>
<PTree :items="items">
<template #app="{ item }">
<p class="font-bold italic">
{{ item.label }}
</p>
</template>
</PTree>
</template>
| Prop | Default | Type |
|---|
| Slot | Type |
|---|
| Event | Type |
|---|
Below is the theme configuration skeleton for the PTree. Since the component is provided unstyled by default, you will need to fill in these values to apply your own custom look and feel. If you prefer to use our pre-built, opinionated styling, you can instead use our UnoCSS preset, this docs is using it as well.
export default defineAppConfig({
pohon: {
tree: {
slots: {
root: '',
item: '',
listWithChildren: '',
itemWithChildren: '',
link: '',
linkLeadingIcon: '',
linkLabel: '',
linkTrailing: '',
linkTrailingIcon: ''
},
variants: {
virtualize: {
true: {
root: ''
}
},
color: {
primary: {
link: ''
},
secondary: {
link: ''
},
success: {
link: ''
},
info: {
link: ''
},
warning: {
link: ''
},
error: {
link: ''
},
neutral: {
link: ''
}
},
size: {
xs: {
listWithChildren: '',
link: '',
linkLeadingIcon: '',
linkTrailingIcon: ''
},
sm: {
listWithChildren: '',
link: '',
linkLeadingIcon: '',
linkTrailingIcon: ''
},
md: {
listWithChildren: '',
link: '',
linkLeadingIcon: '',
linkTrailingIcon: ''
},
lg: {
listWithChildren: '',
link: '',
linkLeadingIcon: '',
linkTrailingIcon: ''
},
xl: {
listWithChildren: '',
link: '',
linkLeadingIcon: '',
linkTrailingIcon: ''
}
},
selected: {
true: {
link: ''
},
false: {
link: ''
}
},
disabled: {
true: {
link: ''
}
}
},
compoundVariants: [],
defaultVariants: {
color: 'primary',
size: 'md'
}
}
}
};
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import pohon from 'pohon-ui/vite'
export default defineAppConfig({
pohon: {
tree: {
slots: {
root: '',
item: '',
listWithChildren: '',
itemWithChildren: '',
link: '',
linkLeadingIcon: '',
linkLabel: '',
linkTrailing: '',
linkTrailingIcon: ''
},
variants: {
virtualize: {
true: {
root: ''
}
},
color: {
primary: {
link: ''
},
secondary: {
link: ''
},
success: {
link: ''
},
info: {
link: ''
},
warning: {
link: ''
},
error: {
link: ''
},
neutral: {
link: ''
}
},
size: {
xs: {
listWithChildren: '',
link: '',
linkLeadingIcon: '',
linkTrailingIcon: ''
},
sm: {
listWithChildren: '',
link: '',
linkLeadingIcon: '',
linkTrailingIcon: ''
},
md: {
listWithChildren: '',
link: '',
linkLeadingIcon: '',
linkTrailingIcon: ''
},
lg: {
listWithChildren: '',
link: '',
linkLeadingIcon: '',
linkTrailingIcon: ''
},
xl: {
listWithChildren: '',
link: '',
linkLeadingIcon: '',
linkTrailingIcon: ''
}
},
selected: {
true: {
link: ''
},
false: {
link: ''
}
},
disabled: {
true: {
link: ''
}
}
},
compoundVariants: [],
defaultVariants: {
color: 'primary',
size: 'md'
}
}
}
};
With Pohon UI, you can achieve similar component functionality with less code and effort, as it comes with built-in styles mechanism and behaviors that are optimized for common use cases. Since it's using unocss-variants it adds a runtime cost, but it can be worth it if you prioritize development speed and ease of use over fine-grained control.
If this is a deal breaker for you, you can always stick to using Akar and build your own custom components on top of it.