Sections
Components
- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Collapsible
- ColorPicker
- Combobox
- Command
- Context Menu
- Curves
- Data Table
- Date Picker
- Dialog
- Drawer
- Dropdown Menu
- Empty
- Field
- FontPicker
- Form
- Gradient Picker
- Hover Card
- Input
- Input Group
- Input OTP
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Native Select
- Navigation Menu
- Number Field
- Pagination
- Pin Input
- Popover
- Progress
- Questionnaire
- Radio Group
- Range Calendar
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner
- Spinner
- Stepper
- Switch
- Table
- Tabs
- Tags Input
- Textarea
- Toast
- Toggle
- Toggle Group
- Tool Rail
- Tooltip
- Tree
- Typography
- Vector Pad
Directory
Forms
Utilities
Main Group
Cylinder
Cylinder.001
Shape
Shape.001
Group
Shape.002
Shape.003
Group.001
Group.002
<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
expandableis 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>