Tree

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

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 }
A unique identifier is required for each item. The component will use the 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>

Nested Soon

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

If an icon is specified for an item, it will always take precedence over these props.
<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>
You can customize this icon globally in your app.config.ts under ui.icons.chevronDown key.
You can customize this icon globally in your vite.config.ts under ui.icons.chevronDown key.

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>
You can customize these icons globally in your app.config.ts under ui.icons.folder and ui.icons.folderOpen keys.
You can customize these icons globally in your 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>
You can also disable individual items using 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>
This lets you expand or collapse a parent item without selecting it.

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>
This lets you select a parent item without expanding or collapsing its children.

With checkbox in items

You can use the item-leading slot to add a Checkbox to the item and the onSelect and onToggle events to control the checked state of the item.

app/
  • app.vue
  • nuxt.config.ts
    <script setup lang="ts">
    import type { TreeItem } from '@nuxt/ui'
    import type { TreeItemSelectEvent, TreeItemToggleEvent } from 'reka-ui'
    
    const items: TreeItem[] = [
      {
        label: 'app/',
        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(event: TreeItemSelectEvent<TreeItem>) {
      if (event.detail.originalEvent.type === 'click') {
        event.preventDefault()
      }
    }
    
    function onToggle(event: TreeItemToggleEvent<TreeItem>) {
      if (event.detail.originalEvent.type === 'keydown') {
        event.preventDefault()
      }
    }
    </script>
    
    <template>
      <UTree
        v-model="value"
        :items="items"
        multiple
        propagate-select
        bubble-select
        @select="onSelect"
        @toggle="onToggle"
      >
        <template #item-leading="{ selected, indeterminate, handleSelect }">
          <UCheckbox
            :model-value="indeterminate ? 'indeterminate' : selected"
            data-tree-select-checkbox="true"
            @change="handleSelect"
            @click.stop
          />
        </template>
      </UTree>
    </template>
    

    With virtualization Soon

    Use the virtualize prop to enable virtualization for large lists as a boolean or an object with options like { estimateSize: 32, overscan: 12 }.

    When virtualization is enabled, the tree structure is flattened, similar to setting the 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

    'ul'

    any

    The element or component this component should render as.

    color

    'primary'

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

    size

    'md'

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

    getKey

    (val: TreeItem): string

    This function is passed the index of each item and should return a unique key for that item

    labelKey

    'label'

    string | number

    The key used to get the label from the item.

    trailingIcon

    appConfig.ui.icons.chevronDown

    string | object

    The icon displayed on the right side of a parent node.

    expandedIcon

    appConfig.ui.icons.folderOpen

    string | object

    The icon displayed when a parent node is expanded.

    collapsedIcon

    appConfig.ui.icons.folder

    string | object

    The icon displayed when a parent node is collapsed.

    items

    TreeItem[]

    modelValue

    TreeItem | TreeItem[]

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

    defaultValue

    TreeItem | TreeItem[]

    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.

    nested

    true

    boolean

    Use nested DOM structure (children inside parents) vs flattened structure (all items at same level). When virtualize is enabled, this is automatically set to false.

    virtualize

    false

    boolean | { overscan?: number ; estimateSize?: number | undefined; } | undefined

    Enable virtualization for large lists. Note: when enabled, the tree structure is flattened like if nested was set to false.

    onSelect

    (e: SelectEvent$3<TreeItem>, item: TreeItem): void

    onToggle

    (e: ToggleEvent<TreeItem>, item: TreeItem): void

    expanded

    string[]

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

    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

    selectionBehavior

    "replace" | "toggle"

    How multiple selection should behave in the collection.

    propagateSelect

    boolean

    When true, selecting parent will select the descendants.

    disabled

    boolean

    When true, prevents the user from interacting with tree

    bubbleSelect

    boolean

    When true, selecting children will update the parent state.

    ui

    { root?: ClassNameValue; item?: ClassNameValue; listWithChildren?: ClassNameValue; itemWithChildren?: ClassNameValue; link?: ClassNameValue; linkLeadingIcon?: ClassNameValue; linkLabel?: ClassNameValue; linkTrailing?: ClassNameValue; linkTrailingIcon?: ClassNameValue; }

    Slots

    Slot Type
    item-wrapper

    { item: TreeItem; index: number; level: number; expanded: boolean; selected: boolean; indeterminate: boolean | undefined; handleSelect: () => void; handleToggle: () => void; }

    item

    { item: TreeItem; index: number; level: number; expanded: boolean; selected: boolean; indeterminate: boolean | undefined; handleSelect: () => void; handleToggle: () => void; }

    item-leading

    { item: TreeItem; index: number; level: number; expanded: boolean; selected: boolean; indeterminate: boolean | undefined; handleSelect: () => void; handleToggle: () => void; }

    item-label

    { item: TreeItem; index: number; level: number; expanded: boolean; selected: boolean; indeterminate: boolean | undefined; handleSelect: () => void; handleToggle: () => void; }

    item-trailing

    { item: TreeItem; index: number; level: number; expanded: boolean; selected: boolean; indeterminate: boolean | undefined; handleSelect: () => void; handleToggle: () => void; }

    Emits

    Event Type
    update:modelValue

    [val: TreeItem | TreeItem[]]

    update:expanded

    [val: string[]]

    Theme

    app.config.ts
    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'
          }
        }
      }
    })
    
    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 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'
              }
            }
          }
        })
      ]
    })
    
    Some colors in compoundVariants are omitted for readability. Check out the source code on GitHub.

    Changelog

    c8b01 — feat: provide additional slot props (#5194)

    c744d — feat: implement virtualization (#5162)

    240ff — fix: remove value-key in favor of get-key (#4999)

    117b4 — fix: improve accessibility (#4945)

    11a03 — fix: dot notation type support for labelKey and valueKey (#4933)

    61b60 — feat: allow passing a component instead of a name (#4766)

    5cb65 — feat: import @nuxt/ui-pro components

    411d9 — feat: add item-wrapper slot (#4521)

    fc24e — fix: add type to button elements for accessibility (#4493)

    836f7 — fix: proxy fallthrough attributes

    f7613 — chore: update dependency reka-ui to ^2.3.0 (v3) (#4234)

    0905b — chore: move back item.class on link

    b9adc — feat: add ui field in items (#4060)

    e6e51 — fix: class should have priority over ui prop

    39c86 — fix: refactor types after @nuxt/module-builder upgrade (#3855)

    3deed — fix: simplify reusable template types (#3836)

    b9983 — fix: improve generic types (#3331)

    ef861 — chore: add eol in script tag to fix syntax highlight