Keyboard shortcut: Ctrl + K
Get started

Tree view

VTreeView shows a hierarchy the reader folds and unfolds, such as files or a site map. It follows the ARIA tree pattern: one tab stop, arrow keys to move, open and close, and typeahead.

Usage

vue
  • Documents
  • Resume.pdf
  • Notes.txt
<script setup lang="ts">
import { ref } from 'vue'
import { VTreeView } from 'vectis-ui'
import type { TreeItem } from 'vectis-ui'
import { audio_file as audioFile, description, image, picture_as_pdf as pdf } from 'vectis-ui/icons'

const items: TreeItem[] = [
  {
    value: 'documents',
    label: 'Documents',
    children: [
      { value: 'resume', label: 'Resume.pdf', icon: pdf },
      {
        value: 'taxes',
        label: 'Taxes',
        children: [
          { value: 'taxes-2025', label: '2025.pdf', icon: pdf },
          { value: 'taxes-2026', label: '2026.pdf', icon: pdf },
        ],
      },
    ],
  },
  {
    value: 'pictures',
    label: 'Pictures',
    children: [
      { value: 'beach', label: 'Beach.jpg', icon: image },
      { value: 'mountain', label: 'Mountain.jpg', icon: image },
    ],
  },
  {
    value: 'music',
    label: 'Music',
    children: [{ value: 'song', label: 'Song.mp3', icon: audioFile }],
  },
  { value: 'notes', label: 'Notes.txt', icon: description },
]

const expanded = ref(['documents'])
</script>

<template>
  <VTreeView v-model:expanded="expanded" :items="items" label="Files" class="tree" />
</template>

<style scoped>
.tree {
  max-inline-size: 22rem;
}
</style>

Examples

Selection

selectionMode="single" selects one node by click, Enter or Space, and v-model holds its value. The chevron folds a branch without selecting it. A disabled node stays reachable by the keyboard.

vue
  • Europe
  • Paris
  • Berlin
  • Madrid

Selected: berlin

<script setup lang="ts">
import { ref } from 'vue'
import { VTreeView, VTypography } from 'vectis-ui'
import type { TreeItem, TreeViewModelValue } from 'vectis-ui'

const items: TreeItem[] = [
  {
    value: 'europe',
    label: 'Europe',
    children: [
      { value: 'paris', label: 'Paris' },
      { value: 'berlin', label: 'Berlin' },
      { value: 'madrid', label: 'Madrid', disabled: true },
    ],
  },
  {
    value: 'asia',
    label: 'Asia',
    children: [
      { value: 'tokyo', label: 'Tokyo' },
      { value: 'seoul', label: 'Seoul' },
    ],
  },
]

const selected = ref<TreeViewModelValue>('berlin')
const expanded = ref(['europe'])
</script>

<template>
  <div class="demo">
    <VTreeView
      v-model="selected"
      v-model:expanded="expanded"
      :items="items"
      selection-mode="single"
      label="Offices"
    />
    <VTypography variant="body-sm" tone="muted">Selected: {{ selected ?? 'none' }}</VTypography>
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-3);
  max-inline-size: 22rem;
}
</style>

Checkboxes

selectionMode="multiple" gives each row a checkbox. Checking a branch checks its subtree, and a branch partly checked shows a dash. The model lists checked nodes in tree order, a branch included once everything under it is checked. Disabled nodes keep their state.

vue
  • Content
  • Read
  • Write
  • Publish
  • Billing
  • Invoices
  • Refunds
  • Audit log

Selected: read, write

<script setup lang="ts">
import { computed, ref } from 'vue'
import { VTreeView, VTypography } from 'vectis-ui'
import type { TreeItem, TreeViewModelValue } from 'vectis-ui'

const items: TreeItem[] = [
  {
    value: 'content',
    label: 'Content',
    children: [
      { value: 'read', label: 'Read' },
      { value: 'write', label: 'Write' },
      { value: 'publish', label: 'Publish' },
    ],
  },
  {
    value: 'billing',
    label: 'Billing',
    children: [
      { value: 'invoices', label: 'Invoices' },
      { value: 'refunds', label: 'Refunds', disabled: true },
    ],
  },
  { value: 'audit', label: 'Audit log' },
]

const selected = ref<TreeViewModelValue>(['read', 'write'])
const expanded = ref(['content', 'billing'])
const summary = computed(() => [selected.value ?? []].flat().join(', ') || 'none')
</script>

<template>
  <div class="demo">
    <VTreeView
      v-model="selected"
      v-model:expanded="expanded"
      :items="items"
      selection-mode="multiple"
      label="Permissions"
    />
    <VTypography variant="body-sm" tone="muted">Selected: {{ summary }}</VTypography>
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  gap: var(--vectis-space-3);
  max-inline-size: 22rem;
}
</style>

A node with href is a link, and current marks the page being viewed. To hand navigation to a router, call preventDefault() on the event that activate receives.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VTreeView } from 'vectis-ui'
import type { TreeItem } from 'vectis-ui'

const items: TreeItem[] = [
  {
    value: 'guides',
    label: 'Guides',
    children: [
      { value: 'installation', label: 'Installation', href: '#links' },
      { value: 'theming', label: 'Theming', href: '#links' },
    ],
  },
  {
    value: 'components',
    label: 'Components',
    children: [
      { value: 'button', label: 'Button', href: '#links' },
      { value: 'tree-view', label: 'Tree view', href: '#links', current: true },
    ],
  },
  { value: 'changelog', label: 'Changelog', href: '#links' },
]

const expanded = ref(['components'])
</script>

<template>
  <VTreeView v-model:expanded="expanded" :items="items" label="Site" size="sm" class="tree" />
</template>

<style scoped>
.tree {
  max-inline-size: 16rem;
}
</style>

Lazy loading

loadChildren fetches the children of a lazy node on its first expansion, and the tree keeps them. A failure closes the node and announces it; the next expansion retries.

vue
<script setup lang="ts">
import { VTreeView } from 'vectis-ui'
import type { TreeItem } from 'vectis-ui'

const items: TreeItem[] = [
  { value: 'eu-west', label: 'eu-west', lazy: true },
  { value: 'us-east', label: 'us-east', lazy: true },
]

// Stands in for a request to your server.
function loadChildren(item: TreeItem): Promise<TreeItem[]> {
  return new Promise((resolve) =>
    setTimeout(
      () =>
        resolve(
          [1, 2, 3].map((n) => ({
            value: `${item.value}-${n}`,
            label: `${item.label}-node-${n}`,
            lazy: n === 1,
          })),
        ),
      800,
    ),
  )
}
</script>

<template>
  <VTreeView :items="items" :load-children="loadChildren" label="Servers" class="tree" />
</template>

<style scoped>
.tree {
  max-inline-size: 22rem;
}
</style>

End content

The #end slot adds content at the end of each row, such as a count. Keep it free of focusable elements: a row is a single control.

vue
  • Inbox12
  • Work9
  • Family3
  • Drafts
  • Spam41
<script setup lang="ts">
import { ref } from 'vue'
import { VBadge, VTreeView } from 'vectis-ui'
import type { TreeItem } from 'vectis-ui'

const items: TreeItem[] = [
  {
    value: 'inbox',
    label: 'Inbox',
    children: [
      { value: 'work', label: 'Work' },
      { value: 'family', label: 'Family' },
    ],
  },
  { value: 'drafts', label: 'Drafts' },
  { value: 'spam', label: 'Spam' },
]

const unread: Record<string, number> = { inbox: 12, work: 9, family: 3, spam: 41 }
const expanded = ref(['inbox'])
</script>

<template>
  <VTreeView v-model:expanded="expanded" :items="items" label="Mail" class="tree">
    <template #end="{ item }">
      <VBadge v-if="unread[item.value]" :count="unread[item.value]" variant="soft" tone="neutral" />
    </template>
  </VTreeView>
</template>

<style scoped>
.tree {
  max-inline-size: 22rem;
}
</style>

API

Props

PropTypeDefault
itemsTreeItem[]none
First level of the tree. Each node has a unique value and a label, and optional icon, children, lazy, href, current and disabled.
selectionModeTreeSelectionMode'none' | 'single' | 'multiple''none'
Whether nodes can be selected: not at all, one at a time, or several with checkboxes.
loadChildren(item: TreeItem) => Promise<TreeItem[]>none
Fetches the children of a lazy node on its first expansion. A failure collapses the node; the next expansion retries.
sizeTreeViewSize'sm' | 'md''md'
Row height.
labelstringnone
Accessible name of the tree. Defaults to the library dictionary.
v-modelTreeViewModelValuenull
Selection: a value or null in single mode, an array in multiple mode. A value given for a branch checks its subtree.
v-model:expandedItemValue[][]
Values of the expanded nodes. Unbound, the tree keeps this state itself.

Events

EventType
activate[item: TreeItem, event: MouseEvent | KeyboardEvent]
A node was clicked or Enter was pressed on it. Receives the node and the event; preventDefault() cancels a link.

Slots

SlotType
iconTreeItemSlotProps
Content replacing the icon. Receives item, level and expanded.
labelTreeItemSlotProps
Content replacing the label. Receives the same props.
endTreeItemSlotProps
Content at the end of the row. Must not be focusable.

Types

Types used in the API tables. Import exported types from vectis-ui.

export interface BuiltinIcon {
  name: string
  paths: readonly [string] | readonly [string, string]
}
export type IconRender =
  | { path: string; viewBox?: string }
  | { component: Component; props?: Record<string, unknown> }
  | { src: string }
  | { text: string; class?: string }
  | { class: string }
export type IconSource = string | BuiltinIcon | IconRender
export type ItemValue = string | number
export interface TreeItem {
  value: ItemValue
  label: string
  icon?: IconSource
  children?: TreeItem[]
  lazy?: boolean
  href?: string
  current?: boolean
  disabled?: boolean
}
export interface TreeItemSlotProps {
  item: TreeItem
  level: number
  expanded: boolean
}
export type TreeViewModelValue = ItemValue | ItemValue[] | null

CSS variables

TokenValue
--vectis-control-size-check1.25rem
--vectis-control-size-check-mark0.875rem