Tree

PreviousNext

A hierarchy of rows with expandable groups, selection, and per-row toggles.

Main Group
Cylinder
Cylinder.001
Shape
Shape.001
Group
Shape.002
Shape.003
<script setup lang="ts">
import { ref } from 'vue'
import { Tree } from '@/components/ui/tree'
import TreeNode from './TreeNode.vue'

interface Node {
  id: string
  label: string
  /** Preview only — in an app the host program supplies the icon per object. */
  kind?: 'cube' | 'cylinder' | 'hex'
  children?: Node[]
}

const nodes: Node[] = [
  {
    id: 'main',
    label: 'Main Group',
    kind: 'hex',
    children: [
      { id: 'cylinder', label: 'Cylinder', kind: 'cylinder' },
      { id: 'cylinder-001', label: 'Cylinder.001', kind: 'cylinder' },
      { id: 'shape', label: 'Shape', kind: 'hex' },
      { id: 'shape-001', label: 'Shape.001', kind: 'hex' },
      {
        id: 'group',
        label: 'Group',
        kind: 'hex',
        children: [
          { id: 'shape-002', label: 'Shape.002', kind: 'hex' },
          { id: 'shape-003', label: 'Shape.003', kind: 'hex' },
          {
            id: 'group-001',
            label: 'Group.001',
            kind: 'hex',
            children: [
              { id: 'cube', label: 'Cube', kind: 'cube' },
              { id: 'cube-001', label: 'Cube.001', kind: 'cube' },
            ],
          },
        ],
      },
      { id: 'group-002', label: 'Group.002', kind: 'hex', children: [{ id: 'circle', label: 'Circle', kind: 'hex' }] },
    ],
  },
]

const expanded = ref(new Set(['main', 'group']))
const selected = ref('shape-001')
const hiddenInScene = ref(new Set(['shape']))

function toggle(id: string, value: boolean) {
  value ? expanded.value.add(id) : expanded.value.delete(id)
  expanded.value = new Set(expanded.value)
}

function toggleVisibility(id: string) {
  hiddenInScene.value.has(id) ? hiddenInScene.value.delete(id) : hiddenInScene.value.add(id)
  hiddenInScene.value = new Set(hiddenInScene.value)
}
</script>

<template>
  <Tree class="max-w-sm">
    <template v-for="node in nodes" :key="node.id">
      <TreeNode
        :node="node"
        :expanded="expanded"
        :selected="selected"
        :hidden-in-scene="hiddenInScene"
        @toggle="toggle"
        @select="selected = $event"
        @visibility="toggleVisibility"
      />
    </template>
  </Tree>
</template>

Installation

pnpm dlx hui-kit@latest add tree

Usage

<script setup lang="ts">
import { ref } from 'vue'
import { Tree, TreeGroup, TreeItem } from '@/components/ui/tree'

const expanded = ref(true)
</script>

<template>
  <Tree>
    <TreeItem v-model:expanded="expanded" expandable selected>
      Main Group
    </TreeItem>

    <TreeGroup v-if="expanded">
      <TreeItem>Cylinder</TreeItem>
      <TreeItem muted>
        Shape
      </TreeItem>
    </TreeGroup>
  </Tree>
</template>

Tree is the container, TreeItem is one 36px row, and TreeGroup is a single indent level — the guide line comes with it. Nesting groups inside groups is what makes depth, so an item never needs to know how deep it sits.

Anatomy

A row has four slots, left to right:

  • the chevron, drawn only when expandable is set — a leaf keeps the space so every label lines up
  • #icon, sized to 20px
  • the default slot, the label
  • #actions, the trailing toggles

Rows are uncontrolled on purpose: TreeItem emits update:expanded and select, and the owner keeps the state. That keeps a tree of any shape — recursive, virtualised, or loaded on demand — the caller's business.

Examples

Actions

Up to four toggles fit in a row before the label starts losing width.

One toggle
Four toggles
<script setup lang="ts">
import { ref } from 'vue'
import { IconAddObjectCube, IconAddObjectCylinder, IconEye, IconEyeOff, IconOpen, IconSelectFace } from '@/components/ui/icons'
import { Tree, TreeItem } from '@/components/ui/tree'

const visible = ref(true)
</script>

<template>
  <Tree class="max-w-sm">
    <TreeItem>
      <template #icon>
        <IconAddObjectCube />
      </template>
      One toggle
      <template #actions>
        <button type="button" aria-label="Toggle visibility" @click="visible = !visible">
          <IconEye v-if="visible" />
          <IconEyeOff v-else />
        </button>
      </template>
    </TreeItem>

    <TreeItem>
      <template #icon>
        <IconAddObjectCylinder />
      </template>
      Four toggles
      <template #actions>
        <IconSelectFace />
        <IconOpen />
        <IconEyeOff />
        <IconEye />
      </template>
    </TreeItem>
  </Tree>
</template>

Muted

An object switched off in the scene stays in the tree but drops to the tertiary foreground.

Shape
Shape.001
Shape.002
<script setup lang="ts">
import { IconAddObjectCube, IconEye, IconEyeOff } from '@/components/ui/icons'
import { Tree, TreeItem } from '@/components/ui/tree'
</script>

<template>
  <Tree class="max-w-sm">
    <TreeItem>
      <template #icon>
        <IconAddObjectCube />
      </template>
      Shape
      <template #actions>
        <IconEye />
      </template>
    </TreeItem>

    <TreeItem muted>
      <template #icon>
        <IconAddObjectCube />
      </template>
      Shape.001
      <template #actions>
        <IconEyeOff />
      </template>
    </TreeItem>

    <TreeItem selected>
      <template #icon>
        <IconAddObjectCube />
      </template>
      Shape.002
      <template #actions>
        <IconEye />
      </template>
    </TreeItem>
  </Tree>
</template>