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">
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>
<script setup lang="ts">
import { ref } from 'vue'
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>

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: TreeItemToggleEvent<TreeItem>) => void
  • onSelect?: (e: TreeItemSelectEvent<TreeItem>) => 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 '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>
<script setup lang="ts">
import { ref } from 'vue'
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>

Multiple

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>
<script setup lang="ts">
import { ref } from 'vue'
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>

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 '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>
<script setup lang="ts">
import { ref } from 'vue'
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>
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 '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>
<script setup lang="ts">
import { ref } from 'vue'
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>

Size

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>
<script setup lang="ts">
import { ref } from 'vue'
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>

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 '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>
<script setup lang="ts">
import { ref } from 'vue'
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>
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 '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>
<script setup lang="ts">
import { ref } from 'vue'
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>
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 '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>
<script setup lang="ts">
import { ref } from 'vue'
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>
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 'pohon-ui';

const items: Array<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>
  <PTree
    v-model="value"
    :items="items"
  />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { TreeItem } from 'pohon-ui';

const items: Array<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>
  <PTree
    v-model="value"
    :items="items"
  />
</template>
Use the get-key prop to change the function used to get the unique key from each item when a v-model or default-value is provided.

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 { TreeItemSelectEvent } from 'akar';
import type { TreeItem } from 'pohon-ui';

const items: Array<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' },
];

function onSelect(e: TreeItemSelectEvent<TreeItem>) {
  if (e.detail.originalEvent.type === 'click') {
    e.preventDefault();
  }
}
</script>

<template>
  <PTree
    :items="items"
    @select="onSelect"
  />
</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 'pohon-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 Array<TreeItem>;

const expanded = ref(['app', 'app/composables']);
</script>

<template>
  <PTree
    v-model:expanded="expanded"
    :items="items"
    :get-key="i => i.id"
  />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { TreeItem } from 'pohon-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 Array<TreeItem>;

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 { TreeItemToggleEvent } from 'akar';
import type { TreeItem } from 'pohon-ui';

const items: Array<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' },
];

function onToggle(e: TreeItemToggleEvent<TreeItem>) {
  if (e.detail.originalEvent.type === 'keydown') {
    e.preventDefault();
  }
}
</script>

<template>
  <PTree
    :items="items"
    @toggle="onToggle"
  />
</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 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 { TreeItemSelectEvent } from 'akar';
import type { TreeItem } from 'pohon-ui';

const items: Array<TreeItem> = [
  {
    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: TreeItemSelectEvent<TreeItem>) {
  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>
<script setup lang="ts">
import { ref } from 'vue'
import type { TreeItemSelectEvent } from 'akar';
import type { TreeItem } from 'pohon-ui';

const items: Array<TreeItem> = [
  {
    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: TreeItemSelectEvent<TreeItem>) {
  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>
This example uses the as prop to change the items from button to div as the Checkbox is also rendered as a button.

With drag and drop

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 { TreeItem } from 'pohon-ui'
import { useSortable } from '@vueuse/integrations/useSortable'

const items = shallowRef<Array<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' }
])

function flatten(
  items: Array<TreeItem>,
  parent = items
): Array<{ item: TreeItem; parent: Array<TreeItem>; 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>
<script setup lang="ts">
import { shallowRef, useTemplateRef } from 'vue'
import type { TreeItem } from 'pohon-ui'
import { useSortable } from '@vueuse/integrations/useSortable'

const items = shallowRef<Array<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' }
])

function flatten(
  items: Array<TreeItem>,
  parent = items
): Array<{ item: TreeItem; parent: Array<TreeItem>; 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>
This example sets the nested prop to false to have a flat list of items so that the items can be dragged and dropped.

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

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 'pohon-ui'

const items: Array<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>
  <PTree 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 '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<TreeItem>;
</script>

<template>
  <PTree :items="items">
    <template #app="{ item }">
      <p class="font-bold italic">
        {{ item.label }}
      </p>
    </template>
  </PTree>
</template>

API

Props

Prop Default Type

Slots

Slot Type

Emits

Event Type

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    tree: {
      slots: {
        root: 'relative isolate',
        item: 'w-full',
        listWithChildren: 'border-s border-border',
        itemWithChildren: 'ps-1.5 -ms-px',
        link: 'group text-sm flex w-full select-none items-center relative focus-visible:outline-none focus:outline-none before:rounded-md before:content-empty before:inset-x-0 before:inset-y-px before:absolute before:-z-1 focus-visible:before:outline-3',
        linkLeadingIcon: 'shrink-0 relative',
        linkLabel: 'truncate',
        linkTrailing: 'ms-auto inline-flex gap-1.5 items-center',
        linkTrailingIcon: 'shrink-0 transition-transform-280 ease-out group-data-[expanded]:rotate-180 motion-reduce:transition-none'
      },
      variants: {
        virtualize: {
          true: {
            root: 'overflow-y-auto'
          }
        },
        color: {
          primary: {
            link: 'before:outline-primary/25'
          },
          secondary: {
            link: 'before:outline-secondary/25'
          },
          success: {
            link: 'before:outline-success/25'
          },
          info: {
            link: 'before:outline-info/25'
          },
          warning: {
            link: 'before:outline-warning/25'
          },
          error: {
            link: 'before:outline-error/25'
          },
          neutral: {
            link: 'before:outline-border-inverted/25'
          }
        },
        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-background-elevated'
          }
        },
        disabled: {
          true: {
            link: 'cursor-not-allowed opacity-75'
          }
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          selected: true,
          class: {
            link: 'color-primary'
          }
        },
        {
          color: 'neutral',
          selected: true,
          class: {
            link: 'color-text-highlighted'
          }
        },
        {
          selected: false,
          disabled: false,
          class: {
            link: [
              'hover:color-text-highlighted hover:before:bg-background-elevated/50',
              'transition-colors before:transition-colors'
            ]
          }
        }
      ],
      defaultVariants: {
        color: 'primary',
        size: 'md'
      }
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from 'pohon-ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        tree: {
          slots: {
            root: 'relative isolate',
            item: 'w-full',
            listWithChildren: 'border-s border-border',
            itemWithChildren: 'ps-1.5 -ms-px',
            link: 'group text-sm flex w-full select-none items-center relative focus-visible:outline-none focus:outline-none before:rounded-md before:content-empty before:inset-x-0 before:inset-y-px before:absolute before:-z-1 focus-visible:before:outline-3',
            linkLeadingIcon: 'shrink-0 relative',
            linkLabel: 'truncate',
            linkTrailing: 'ms-auto inline-flex gap-1.5 items-center',
            linkTrailingIcon: 'shrink-0 transition-transform-280 ease-out group-data-[expanded]:rotate-180 motion-reduce:transition-none'
          },
          variants: {
            virtualize: {
              true: {
                root: 'overflow-y-auto'
              }
            },
            color: {
              primary: {
                link: 'before:outline-primary/25'
              },
              secondary: {
                link: 'before:outline-secondary/25'
              },
              success: {
                link: 'before:outline-success/25'
              },
              info: {
                link: 'before:outline-info/25'
              },
              warning: {
                link: 'before:outline-warning/25'
              },
              error: {
                link: 'before:outline-error/25'
              },
              neutral: {
                link: 'before:outline-border-inverted/25'
              }
            },
            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-background-elevated'
              }
            },
            disabled: {
              true: {
                link: 'cursor-not-allowed opacity-75'
              }
            }
          },
          compoundVariants: [
            {
              color: 'primary',
              selected: true,
              class: {
                link: 'color-primary'
              }
            },
            {
              color: 'neutral',
              selected: true,
              class: {
                link: 'color-text-highlighted'
              }
            },
            {
              selected: false,
              disabled: false,
              class: {
                link: [
                  'hover:color-text-highlighted hover:before:bg-background-elevated/50',
                  'transition-colors before:transition-colors'
                ]
              }
            }
          ],
          defaultVariants: {
            color: 'primary',
            size: 'md'
          }
        }
      }
    })
  ]
})
Some colors in compoundVariants are omitted for readability. Check out the source code on GitHub.

Changelog

No recent changes