Usage
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>
<UTree :items="items" />
</template>
Items
Use the items
prop as an array of objects with the following properties:
icon?: string
label?: string
trailingIcon?: string
defaultExpanded?: boolean
disabled?: boolean
slot?: string
children?: TreeItem[]
onToggle?(e: Event): void
onSelect?(e?: Event): void
class?: any
ui?: { 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 '@nuxt/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>
<UTree :items="items" />
</template>
Multiple
Use the multiple
prop to allow multiple item selections.
<script setup lang="ts">
import type { TreeItem } from '@nuxt/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>
<UTree multiple :items="items" />
</template>
Soon Nested
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 '@nuxt/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>
<UTree :nested="false" :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.Color
Use the color
prop to change the color of the Tree.
<script setup lang="ts">
import type { TreeItem } from '@nuxt/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>
<UTree color="neutral" :items="items" />
</template>
Size
Use the size
prop to change the size of the Tree.
<script setup lang="ts">
import type { TreeItem } from '@nuxt/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>
<UTree size="xl" :items="items" />
</template>
Trailing Icon
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 '@nuxt/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>
<UTree trailing-icon="i-lucide-arrow-down" :items="items" />
</template>
Expanded Icon
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 '@nuxt/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>
<UTree expanded-icon="i-lucide-book-open" collapsed-icon="i-lucide-book" :items="items" />
</template>
app.config.ts
under ui.icons.folder
and ui.icons.folderOpen
keys.vite.config.ts
under ui.icons.folder
and ui.icons.folderOpen
keys.Disabled
Use the disabled
prop to prevent any user interaction with the Tree.
<script setup lang="ts">
import type { TreeItem } from '@nuxt/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>
<UTree disabled :items="items" />
</template>
item.disabled
.Examples
Control selected item(s)
You can control the selected item(s) by using the default-value
prop or the v-model
directive.
<script setup lang="ts">
import type { TreeItem } from '@nuxt/ui'
const items: 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' }
]
const value = ref()
</script>
<template>
<UTree v-model="value" :items="items" />
</template>
If you want to prevent an item from being selected, you can use the item.onSelect()
property:
<script setup lang="ts">
import type { TreeItem } from '@nuxt/ui'
const items: TreeItem[] = [
{
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' }
]
</script>
<template>
<UTree :items="items" />
</template>
Control expanded items
You can control the expanded items by using the default-expanded
prop or the v-model
directive.
<script setup lang="ts">
import type { TreeItem } from '@nuxt/ui'
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 TreeItem[]
const expanded = ref(['app', 'app/composables'])
</script>
<template>
<UTree 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:
<script setup lang="ts">
import type { TreeItem } from '@nuxt/ui'
const items: TreeItem[] = [
{
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' }
]
</script>
<template>
<UTree :items="items" />
</template>
Soon With virtualization
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 { TreeItem } from '@nuxt/ui'
const items: TreeItem[] = 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>
<UTree virtualize :items="items" class="h-80" />
</template>
With custom slot
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 { TreeItem } from '@nuxt/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 TreeItem[]
</script>
<template>
<UTree :items="items">
<template #app="{ item }">
<p class="italic font-bold">
{{ item.label }}
</p>
</template>
</UTree>
</template>
API
Props
Prop | Default | Type |
---|---|---|
as |
|
The element or component this component should render as. |
color |
|
|
size |
|
|
getKey |
This function is passed the index of each item and should return a unique key for that item | |
labelKey |
|
The key used to get the label from the item. |
trailingIcon |
|
The icon displayed on the right side of a parent node. |
expandedIcon |
|
The icon displayed when a parent node is expanded. |
collapsedIcon |
|
The icon displayed when a parent node is collapsed. |
items |
| |
modelValue |
The controlled value of the Tree. Can be bind as
| |
defaultValue |
The value of the Tree when initially rendered. Use when you do not need to control the state of the Tree.
| |
multiple |
Whether multiple options can be selected or not. | |
nested |
|
Use nested DOM structure (children inside parents) vs flattened structure (all items at same level).
When |
virtualize |
|
Enable virtualization for large lists.
Note: when enabled, the tree structure is flattened like if
|
expanded |
The controlled value of the expanded item. Can be binded with with | |
defaultExpanded |
The value of the expanded tree when initially rendered. Use when you do not need to control the state of the expanded tree | |
selectionBehavior |
How multiple selection should behave in the collection. | |
propagateSelect |
When | |
disabled |
When | |
bubbleSelect |
When | |
ui |
|
Slots
Slot | Type |
---|---|
item-wrapper |
|
item |
|
item-leading |
|
item-label |
|
item-trailing |
|
Emits
Event | Type |
---|---|
update:modelValue |
|
update:expanded |
|
Theme
export default defineAppConfig({
ui: {
tree: {
slots: {
root: 'relative isolate',
item: 'w-full',
listWithChildren: 'border-s border-default',
itemWithChildren: 'ps-1.5 -ms-px',
link: 'relative group w-full flex items-center text-sm before:absolute before:inset-y-px before:inset-x-0 before:z-[-1] before:rounded-md focus:outline-none focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2',
linkLeadingIcon: 'shrink-0 relative',
linkLabel: 'truncate',
linkTrailing: 'ms-auto inline-flex gap-1.5 items-center',
linkTrailingIcon: 'shrink-0 transform transition-transform duration-200 group-data-[expanded=true]:rotate-180'
},
variants: {
virtualize: {
true: {
root: 'overflow-y-auto'
}
},
color: {
primary: {
link: 'focus-visible:before:ring-primary'
},
secondary: {
link: 'focus-visible:before:ring-secondary'
},
success: {
link: 'focus-visible:before:ring-success'
},
info: {
link: 'focus-visible:before:ring-info'
},
warning: {
link: 'focus-visible:before:ring-warning'
},
error: {
link: 'focus-visible:before:ring-error'
},
neutral: {
link: 'focus-visible:before:ring-inverted'
}
},
size: {
xs: {
listWithChildren: 'ms-4',
link: 'px-2 py-1 text-xs gap-1',
linkLeadingIcon: 'size-4',
linkTrailingIcon: 'size-4'
},
sm: {
listWithChildren: 'ms-4.5',
link: 'px-2.5 py-1.5 text-xs gap-1.5',
linkLeadingIcon: 'size-4',
linkTrailingIcon: 'size-4'
},
md: {
listWithChildren: 'ms-5',
link: 'px-2.5 py-1.5 text-sm gap-1.5',
linkLeadingIcon: 'size-5',
linkTrailingIcon: 'size-5'
},
lg: {
listWithChildren: 'ms-5.5',
link: 'px-3 py-2 text-sm gap-2',
linkLeadingIcon: 'size-5',
linkTrailingIcon: 'size-5'
},
xl: {
listWithChildren: 'ms-6',
link: 'px-3 py-2 text-base gap-2',
linkLeadingIcon: 'size-6',
linkTrailingIcon: 'size-6'
}
},
selected: {
true: {
link: 'before:bg-elevated'
},
false: {
link: [
'hover:not-disabled:text-highlighted hover:not-disabled:before:bg-elevated/50',
'transition-colors before:transition-colors'
]
}
},
disabled: {
true: {
link: 'cursor-not-allowed opacity-75'
}
}
},
compoundVariants: [
{
color: 'primary',
selected: true,
class: {
link: 'text-primary'
}
},
{
color: 'neutral',
selected: true,
class: {
link: 'text-highlighted'
}
}
],
defaultVariants: {
color: 'primary',
size: 'md'
}
}
}
})
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
tree: {
slots: {
root: 'relative isolate',
item: 'w-full',
listWithChildren: 'border-s border-default',
itemWithChildren: 'ps-1.5 -ms-px',
link: 'relative group w-full flex items-center text-sm before:absolute before:inset-y-px before:inset-x-0 before:z-[-1] before:rounded-md focus:outline-none focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2',
linkLeadingIcon: 'shrink-0 relative',
linkLabel: 'truncate',
linkTrailing: 'ms-auto inline-flex gap-1.5 items-center',
linkTrailingIcon: 'shrink-0 transform transition-transform duration-200 group-data-[expanded=true]:rotate-180'
},
variants: {
virtualize: {
true: {
root: 'overflow-y-auto'
}
},
color: {
primary: {
link: 'focus-visible:before:ring-primary'
},
secondary: {
link: 'focus-visible:before:ring-secondary'
},
success: {
link: 'focus-visible:before:ring-success'
},
info: {
link: 'focus-visible:before:ring-info'
},
warning: {
link: 'focus-visible:before:ring-warning'
},
error: {
link: 'focus-visible:before:ring-error'
},
neutral: {
link: 'focus-visible:before:ring-inverted'
}
},
size: {
xs: {
listWithChildren: 'ms-4',
link: 'px-2 py-1 text-xs gap-1',
linkLeadingIcon: 'size-4',
linkTrailingIcon: 'size-4'
},
sm: {
listWithChildren: 'ms-4.5',
link: 'px-2.5 py-1.5 text-xs gap-1.5',
linkLeadingIcon: 'size-4',
linkTrailingIcon: 'size-4'
},
md: {
listWithChildren: 'ms-5',
link: 'px-2.5 py-1.5 text-sm gap-1.5',
linkLeadingIcon: 'size-5',
linkTrailingIcon: 'size-5'
},
lg: {
listWithChildren: 'ms-5.5',
link: 'px-3 py-2 text-sm gap-2',
linkLeadingIcon: 'size-5',
linkTrailingIcon: 'size-5'
},
xl: {
listWithChildren: 'ms-6',
link: 'px-3 py-2 text-base gap-2',
linkLeadingIcon: 'size-6',
linkTrailingIcon: 'size-6'
}
},
selected: {
true: {
link: 'before:bg-elevated'
},
false: {
link: [
'hover:not-disabled:text-highlighted hover:not-disabled:before:bg-elevated/50',
'transition-colors before:transition-colors'
]
}
},
disabled: {
true: {
link: 'cursor-not-allowed opacity-75'
}
}
},
compoundVariants: [
{
color: 'primary',
selected: true,
class: {
link: 'text-primary'
}
},
{
color: 'neutral',
selected: true,
class: {
link: 'text-highlighted'
}
}
],
defaultVariants: {
color: 'primary',
size: 'md'
}
}
}
})
]
})