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

Usage

Items

Use the items prop as an array of objects with the following properties:

  • value?: string - Unique identifier for the item
  • label?: string - Display text for the item
  • icon?: string - Icon to display before the label
  • leadingIcon?: string - Alternative icon before the label
  • trailingIcon?: string - Icon to display after the label
  • disabled?: boolean - Whether the item is disabled
  • defaultOpen?: boolean - Whether the item is expanded by default
  • children?: TreeItem[] - Nested items
  • slot?: string - Custom slot name for item content
A unique identifier is required for each item. The component will use the value prop as identifier, falling back to label if value is not provided. One of these must be provided for the component to work properly.
  • app
  • app.vue
  • nuxt.config.ts
<script setup lang="ts">
const items = ref([
  {
    label: 'app',
    icon: 'i-lucide-folder',
    defaultOpen: true,
    children: [
      {
        label: 'composables',
        icon: 'i-lucide-folder',
        children: [
          {
            label: 'useAuth.ts',
            icon: 'vscode-icons:file-type-typescript'
          },
          {
            label: 'useUser.ts',
            icon: 'vscode-icons:file-type-typescript'
          }
        ]
      },
      {
        label: 'components',
        icon: 'i-lucide-folder',
        children: [
          {
            label: 'Home',
            icon: 'i-lucide-folder',
            children: [
              {
                label: 'Card.vue',
                icon: 'vscode-icons:file-type-vue'
              },
              {
                label: 'Button.vue',
                icon: 'vscode-icons:file-type-vue'
              }
            ]
          }
        ]
      }
    ]
  },
  {
    label: 'app.vue',
    icon: 'vscode-icons:file-type-vue'
  },
  {
    label: 'nuxt.config.ts',
    icon: 'vscode-icons:file-type-nuxt'
  }
])
</script>

<template>
  <UTree :items="items" />
</template>

Color

Use the color prop to change the color of the Tree.

  • app
  • app.vue
  • nuxt.config.ts
<script setup lang="ts">
const items = ref([
  {
    label: 'app',
    icon: 'i-lucide-folder',
    defaultOpen: true,
    children: [
      {
        label: 'composables',
        icon: 'i-lucide-folder',
        children: [
          {
            label: 'useAuth.ts',
            icon: 'vscode-icons:file-type-typescript'
          },
          {
            label: 'useUser.ts',
            icon: 'vscode-icons:file-type-typescript'
          }
        ]
      },
      {
        label: 'components',
        icon: 'i-lucide-folder',
        children: [
          {
            label: 'Home',
            icon: 'i-lucide-folder',
            children: [
              {
                label: 'Card.vue',
                icon: 'vscode-icons:file-type-vue'
              },
              {
                label: 'Button.vue',
                icon: 'vscode-icons:file-type-vue'
              }
            ]
          }
        ]
      }
    ]
  },
  {
    label: 'app.vue',
    icon: 'vscode-icons:file-type-vue'
  },
  {
    label: 'nuxt.config.ts',
    icon: 'vscode-icons:file-type-nuxt'
  }
])
</script>

<template>
  <UTree color="info" :items="items" />
</template>

Variant

Use the variant prop to change the variant of the Tree.

  • app
  • app.vue
  • nuxt.config.ts
<script setup lang="ts">
const items = ref([
  {
    label: 'app',
    icon: 'i-lucide-folder',
    defaultOpen: true,
    children: [
      {
        label: 'composables',
        icon: 'i-lucide-folder',
        children: [
          {
            label: 'useAuth.ts',
            icon: 'vscode-icons:file-type-typescript'
          },
          {
            label: 'useUser.ts',
            icon: 'vscode-icons:file-type-typescript'
          }
        ]
      },
      {
        label: 'components',
        icon: 'i-lucide-folder',
        children: [
          {
            label: 'Home',
            icon: 'i-lucide-folder',
            children: [
              {
                label: 'Card.vue',
                icon: 'vscode-icons:file-type-vue'
              },
              {
                label: 'Button.vue',
                icon: 'vscode-icons:file-type-vue'
              }
            ]
          }
        ]
      }
    ]
  },
  {
    label: 'app.vue',
    icon: 'vscode-icons:file-type-vue'
  },
  {
    label: 'nuxt.config.ts',
    icon: 'vscode-icons:file-type-nuxt'
  }
])
</script>

<template>
  <UTree variant="link" color="info" :items="items" />
</template>

Size

Use the size prop to change the size of the Tree.

  • app
  • app.vue
  • nuxt.config.ts
<script setup lang="ts">
const items = ref([
  {
    label: 'app',
    icon: 'i-lucide-folder',
    defaultOpen: true,
    children: [
      {
        label: 'composables',
        icon: 'i-lucide-folder',
        children: [
          {
            label: 'useAuth.ts',
            icon: 'vscode-icons:file-type-typescript'
          },
          {
            label: 'useUser.ts',
            icon: 'vscode-icons:file-type-typescript'
          }
        ]
      },
      {
        label: 'components',
        icon: 'i-lucide-folder',
        children: [
          {
            label: 'Home',
            icon: 'i-lucide-folder',
            children: [
              {
                label: 'Card.vue',
                icon: 'vscode-icons:file-type-vue'
              },
              {
                label: 'Button.vue',
                icon: 'vscode-icons:file-type-vue'
              }
            ]
          }
        ]
      }
    ]
  },
  {
    label: 'app.vue',
    icon: 'vscode-icons:file-type-vue'
  },
  {
    label: 'nuxt.config.ts',
    icon: 'vscode-icons:file-type-nuxt'
  }
])
</script>

<template>
  <UTree :items="items" />
</template>

Multiple

Use the multiple prop to allow multiple item selections.

  • app
  • app.vue
  • nuxt.config.ts
<script setup lang="ts">
const items = ref([
  {
    label: 'app',
    icon: 'i-lucide-folder',
    defaultOpen: true,
    children: [
      {
        label: 'composables',
        icon: 'i-lucide-folder',
        children: [
          {
            label: 'useAuth.ts',
            icon: 'vscode-icons:file-type-typescript'
          },
          {
            label: 'useUser.ts',
            icon: 'vscode-icons:file-type-typescript'
          }
        ]
      },
      {
        label: 'components',
        icon: 'i-lucide-folder',
        children: [
          {
            label: 'Home',
            icon: 'i-lucide-folder',
            children: [
              {
                label: 'Card.vue',
                icon: 'vscode-icons:file-type-vue'
              },
              {
                label: 'Button.vue',
                icon: 'vscode-icons:file-type-vue'
              }
            ]
          }
        ]
      }
    ]
  },
  {
    label: 'app.vue',
    icon: 'vscode-icons:file-type-vue'
  },
  {
    label: 'nuxt.config.ts',
    icon: 'vscode-icons:file-type-nuxt'
  }
])
</script>

<template>
  <UTree multiple :items="items" />
</template>

Disabled

Use the disabled prop to disable the entire tree component. This will prevent any user interaction with the tree.

You can also disable individual items using item.disabled.
  • app
  • app.vue
  • nuxt.config.ts
<script setup lang="ts">
const items = ref([
  {
    label: 'app',
    icon: 'i-lucide-folder',
    defaultOpen: true,
    children: [
      {
        label: 'composables',
        icon: 'i-lucide-folder',
        children: [
          {
            label: 'useAuth.ts',
            icon: 'vscode-icons:file-type-typescript'
          },
          {
            label: 'useUser.ts',
            icon: 'vscode-icons:file-type-typescript'
          }
        ]
      },
      {
        label: 'components',
        icon: 'i-lucide-folder',
        children: [
          {
            label: 'Home',
            icon: 'i-lucide-folder',
            children: [
              {
                label: 'Card.vue',
                icon: 'vscode-icons:file-type-vue'
              },
              {
                label: 'Button.vue',
                icon: 'vscode-icons:file-type-vue'
              }
            ]
          }
        ]
      }
    ]
  },
  {
    label: 'app.vue',
    icon: 'vscode-icons:file-type-vue'
  },
  {
    label: 'nuxt.config.ts',
    icon: 'vscode-icons:file-type-nuxt'
  }
])
</script>

<template>
  <UTree disabled :items="items" />
</template>

Icon

Use icon, leadingIcon or trailingIcon to set the default icons for nodes. For parent nodes, use parentIcon, parentLeadingIcon or parentTrailingIcon.

If an icon is specified for an item, it will always take precedence over these props.
  • app
  • app.vue
  • nuxt.config.ts
<script setup lang="ts">
const items = ref([
  {
    label: 'app',
    defaultOpen: true,
    children: [
      {
        label: 'composables',
        children: [
          {
            label: 'useAuth.ts'
          },
          {
            label: 'useUser.ts'
          }
        ]
      },
      {
        label: 'components',
        children: [
          {
            label: 'Home',
            children: [
              {
                label: 'Card.vue'
              },
              {
                label: 'Button.vue'
              }
            ]
          }
        ]
      }
    ]
  },
  {
    label: 'app.vue'
  },
  {
    label: 'nuxt.config.ts'
  }
])
</script>

<template>
  <UTree icon="i-lucide-dot" parent-icon="i-lucide-folder" :items="items" />
</template>

Examples

With rotating icon

You can animate the parent icon using the ui prop.

  • app
  • app.vue
  • nuxt.config.ts
<script setup lang="ts">
const items = [
  {
    label: 'app',
    defaultOpen: true,
    children: [{
      label: 'composables',
      children: [
        { label: 'useAuth.ts', icon: 'vscode-icons:file-type-typescript' },
        { label: 'useUser.ts', icon: 'vscode-icons:file-type-typescript' }
      ]
    },
    {
      label: 'components',
      children: [
        {
          label: 'Home',
          children: [
            { label: 'Card.vue', icon: 'vscode-icons:file-type-vue' },
            { label: 'Button.vue', icon: 'vscode-icons:file-type-vue' }
          ]
        }
      ]
    }]
  },
  { label: 'app.vue', icon: 'vscode-icons:file-type-vue' },
  { label: 'nuxt.config.ts', icon: 'vscode-icons:file-type-nuxt' }
]
</script>

<template>
  <UTree :items="items" parent-icon="lucide:chevron-right" :ui="{ itemLeadingIcon: 'group-data-[expanded]:rotate-90 transition-transform duration-200' }" />
</template>

With expand icon

You can change the icon based on the item state using slots.

  • app
  • app.vue
  • nuxt.config.ts
<script setup lang="ts">
const items = [
  {
    label: 'app',
    icon: 'lucide:folder',
    defaultOpen: true,
    children: [{
      label: 'composables',
      icon: 'lucide:folder',
      children: [
        { label: 'useAuth.ts', icon: 'vscode-icons:file-type-typescript' },
        { label: 'useUser.ts', icon: 'vscode-icons:file-type-typescript' }
      ]
    },
    {
      label: 'components',
      icon: 'lucide:folder',
      children: [
        {
          label: 'Home',
          icon: 'lucide:folder',
          children: [
            { label: 'Card.vue', icon: 'vscode-icons:file-type-vue' },
            { label: 'Button.vue', icon: 'vscode-icons:file-type-vue' }
          ]
        }
      ]
    }]
  },
  { label: 'app.vue', icon: 'vscode-icons:file-type-vue' },
  { label: 'nuxt.config.ts', icon: 'vscode-icons:file-type-nuxt' }
]
</script>

<template>
  <UTree :items="items">
    <template #item-leading="{ hasChildren, expanded }">
      <UIcon v-if="hasChildren && expanded" name="lucide:folder-open" />
      <UIcon v-else-if="hasChildren" name="lucide:folder" />
    </template>
  </UTree>
</template>

With custom slot

Use the item.slot property to customize a specific item.

  • app

  • app.vue
  • nuxt.config.ts
<script setup lang="ts">
const items = [
  {
    label: 'app',
    slot: 'app',
    defaultOpen: true,
    children: [{
      label: 'composables',
      children: [
        { label: 'useAuth.ts', icon: 'vscode-icons:file-type-typescript' },
        { label: 'useUser.ts', icon: 'vscode-icons:file-type-typescript' }
      ]
    },
    {
      label: 'components',
      children: [
        {
          label: 'Home',
          children: [
            { label: 'Card.vue', icon: 'vscode-icons:file-type-vue' },
            { label: 'Button.vue', icon: 'vscode-icons:file-type-vue' }
          ]
        }
      ]
    }]
  },
  { label: 'app.vue', icon: 'vscode-icons:file-type-vue' },
  { label: 'nuxt.config.ts', icon: 'vscode-icons:file-type-nuxt' }
]
</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

'div'

any

The element or component this component should render as.

color

'primary'

"error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"

size

'md'

"md" | "xs" | "sm" | "lg" | "xl"

variant

'pill'

"link" | "pill"

valueKey

'value'

string

The key used to get the value from the item.

labelKey

'label'

string

The key used to get the label from the item.

parentIcon

string

The default leading icon displayed on parent nodes.

parentLeadingIcon

string

The default leading icon displayed on parent nodes.

parentTrailingIcon

string

The default trailing icon displayed on parent nodes.

icon

string

The default leading icon displayed on all nodes.

leadingIcon

string

The default leading icon displayed on all nodes.

trailingIcon

string

The default trailing icon displayed on all nodes.

items

Record<string, any>[]

modelValue

Partial<Record<string, any>> | (Record<string, any> | undefined)[]

The controlled value of the Tree. Can be bind as v-model.

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.

multiple

boolean

Whether multiple options can be selected or not.

disabled

boolean

When true, prevents the user from interacting with tree

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.

expanded

string[]

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

selectionBehavior

"toggle" | "replace"

How multiple selection should behave in the collection.

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

getChildren

(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

propagateSelect

boolean

When true, selecting parent will select the descendants.

ui

PartialString<{ root: string; item: string[]; itemLeadingIcon: string; itemTrailing: string; itemTrailingIcon: string; itemLabel: string; }>

Slots

Slot Type
item

{ item: Record<string, any>; index: number; level: number; hasChildren: boolean; expanded: boolean; selected: boolean; }

item-leading

{ item: Record<string, any>; index: number; level: number; hasChildren: boolean; expanded: boolean; selected: boolean; }

item-label

{ item: Record<string, any>; index: number; level: number; hasChildren: boolean; expanded: boolean; selected: boolean; }

item-trailing

{ item: Record<string, any>; index: number; level: number; hasChildren: boolean; expanded: boolean; selected: boolean; }

Emits

Event Type
update:modelValue

[payload: Record<string, any> | Record<string, any>[]]

update:expanded

[val: string[]]

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    tree: {
      slots: {
        root: 'relative',
        item: [
          'group relative w-full flex items-center select-none rounded-[calc(var(--ui-radius)*1.5)] data-disabled:cursor-not-allowed data-disabled:opacity-75 text-(--ui-text) data-disabled:bg-transparent data-disabled:text-(--ui-text-elevated) outline-none',
          'transition-colors before:transition-colors'
        ],
        itemLeadingIcon: 'shrink-0',
        itemTrailing: 'ms-auto inline-flex',
        itemTrailingIcon: 'shrink-0',
        itemLabel: 'truncate'
      },
      variants: {
        color: {
          primary: 'focus-visible:ring-2 focus-visible:ring-(--ui-primary)',
          secondary: 'focus-visible:ring-2 focus-visible:ring-(--ui-secondary)',
          success: 'focus-visible:ring-2 focus-visible:ring-(--ui-success)',
          info: 'focus-visible:ring-2 focus-visible:ring-(--ui-info)',
          warning: 'focus-visible:ring-2 focus-visible:ring-(--ui-warning)',
          error: 'focus-visible:ring-2 focus-visible:ring-(--ui-error)',
          neutral: ''
        },
        variant: {
          pill: {
            item: 'hover:not-data-disabled:not-data-selected:bg-(--ui-bg-elevated) hover:not-data-disabled:not-data-selected:text-(--ui-text-highlighted)'
          },
          link: {
            item: 'hover:not-data-disabled:not-data-selected:text-(--ui-text-highlighted)'
          }
        },
        size: {
          xs: {
            root: 'text-xs',
            item: 'p-0.75 gap-1',
            itemLeadingIcon: 'size-3',
            itemTrailingIcon: 'size-3'
          },
          sm: {
            root: 'text-xs',
            item: 'p-0.75 gap-1.5',
            itemLeadingIcon: 'size-4.5',
            itemTrailingIcon: 'size-4.5'
          },
          md: {
            root: 'text-sm',
            item: 'p-1 gap-1.5',
            itemLeadingIcon: 'size-4',
            itemTrailingIcon: 'size-4'
          },
          lg: {
            root: 'text-sm',
            item: 'p-1.25 gap-1.5',
            itemLeadingIcon: 'size-4.5',
            itemTrailingIcon: 'size-4.5'
          },
          xl: {
            root: 'text-base',
            item: 'p-1.25 gap-1.5',
            itemLeadingIcon: 'size-5',
            itemTrailingIcon: 'size-5'
          }
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          variant: 'pill',
          class: {
            item: 'data-selected:bg-(--ui-primary)/10 data-selected:text-(--ui-primary)'
          }
        },
        {
          color: 'neutral',
          variant: 'pill',
          class: {
            item: 'data-selected:bg-(--ui-bg-elevated)/50 data-selected:text-(--ui-text-highlighted)'
          }
        },
        {
          color: 'primary',
          variant: 'link',
          class: {
            item: 'data-selected:text-(--ui-primary)'
          }
        },
        {
          color: 'neutral',
          variant: 'link',
          class: {
            item: 'data-selected:text-(--ui-text-highlighted)'
          }
        }
      ],
      defaultVariants: {
        color: 'primary',
        variant: 'pill',
        size: 'md'
      }
    }
  }
})
vite.config.ts
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',
            item: [
              'group relative w-full flex items-center select-none rounded-[calc(var(--ui-radius)*1.5)] data-disabled:cursor-not-allowed data-disabled:opacity-75 text-(--ui-text) data-disabled:bg-transparent data-disabled:text-(--ui-text-elevated) outline-none',
              'transition-colors before:transition-colors'
            ],
            itemLeadingIcon: 'shrink-0',
            itemTrailing: 'ms-auto inline-flex',
            itemTrailingIcon: 'shrink-0',
            itemLabel: 'truncate'
          },
          variants: {
            color: {
              primary: 'focus-visible:ring-2 focus-visible:ring-(--ui-primary)',
              secondary: 'focus-visible:ring-2 focus-visible:ring-(--ui-secondary)',
              success: 'focus-visible:ring-2 focus-visible:ring-(--ui-success)',
              info: 'focus-visible:ring-2 focus-visible:ring-(--ui-info)',
              warning: 'focus-visible:ring-2 focus-visible:ring-(--ui-warning)',
              error: 'focus-visible:ring-2 focus-visible:ring-(--ui-error)',
              neutral: ''
            },
            variant: {
              pill: {
                item: 'hover:not-data-disabled:not-data-selected:bg-(--ui-bg-elevated) hover:not-data-disabled:not-data-selected:text-(--ui-text-highlighted)'
              },
              link: {
                item: 'hover:not-data-disabled:not-data-selected:text-(--ui-text-highlighted)'
              }
            },
            size: {
              xs: {
                root: 'text-xs',
                item: 'p-0.75 gap-1',
                itemLeadingIcon: 'size-3',
                itemTrailingIcon: 'size-3'
              },
              sm: {
                root: 'text-xs',
                item: 'p-0.75 gap-1.5',
                itemLeadingIcon: 'size-4.5',
                itemTrailingIcon: 'size-4.5'
              },
              md: {
                root: 'text-sm',
                item: 'p-1 gap-1.5',
                itemLeadingIcon: 'size-4',
                itemTrailingIcon: 'size-4'
              },
              lg: {
                root: 'text-sm',
                item: 'p-1.25 gap-1.5',
                itemLeadingIcon: 'size-4.5',
                itemTrailingIcon: 'size-4.5'
              },
              xl: {
                root: 'text-base',
                item: 'p-1.25 gap-1.5',
                itemLeadingIcon: 'size-5',
                itemTrailingIcon: 'size-5'
              }
            }
          },
          compoundVariants: [
            {
              color: 'primary',
              variant: 'pill',
              class: {
                item: 'data-selected:bg-(--ui-primary)/10 data-selected:text-(--ui-primary)'
              }
            },
            {
              color: 'neutral',
              variant: 'pill',
              class: {
                item: 'data-selected:bg-(--ui-bg-elevated)/50 data-selected:text-(--ui-text-highlighted)'
              }
            },
            {
              color: 'primary',
              variant: 'link',
              class: {
                item: 'data-selected:text-(--ui-primary)'
              }
            },
            {
              color: 'neutral',
              variant: 'link',
              class: {
                item: 'data-selected:text-(--ui-text-highlighted)'
              }
            }
          ],
          defaultVariants: {
            color: 'primary',
            variant: 'pill',
            size: 'md'
          }
        }
      }
    })
  ]
})
Some colors in compoundVariants are omitted for readability. Check out the source code on GitHub.