GitHub44X

Tree View

Thin wrappers around Reka UI Tree primitives for accessible hierarchical lists.

PreviousNext
      
      <script setup lang="ts">
import { TreeView, TreeViewIndicator, TreeViewItem } from "@/components/tree-view";
import { FileIcon } from "lucide-vue-next";
import { ref } from "vue";
import { TypeScriptIcon, ViteIcon, VueDotjsIcon } from "vue3-simple-icons";

type TreeNode = {
  id: string;
  title: string;
  children?: TreeNode[];
};

const items: TreeNode[] = [
  {
    id: "src",
    title: "src",
    children: [
      {
        id: "components",
        title: "components",
        children: [
          {
            id: "ui",
            title: "ui",
            children: [
              { id: "button", title: "Button.vue" },
              { id: "input", title: "Input.vue" },
              { id: "select", title: "Select.vue" },
            ],
          },
        ],
      },
      {
        id: "pages",
        title: "pages",
        children: [
          { id: "home", title: "Home.vue" },
          { id: "about", title: "About.vue" },
          { id: "contact", title: "Contact.vue" },
        ],
      },
      {
        id: "App.vue",
        title: "App.vue",
      },
      {
        id: "main.ts",
        title: "main.ts",
      },
    ],
  },
  {
    id: "vite.config.ts",
    title: "vite.config.ts",
  },
];

function getFileIcon(filename: string) {
  if (filename.endsWith(".vue")) {
    return h(VueDotjsIcon, { class: "size-4 text-[#4FC08D]" });
  }

  if (filename === "vite.config.ts") {
    return h(ViteIcon, { class: "size-4 text-[#9135FF]" });
  }

  if (filename.endsWith(".ts")) {
    return h(TypeScriptIcon, { class: "size-4 text-[#3178C6]" });
  }

  return h(FileIcon, { class: "size-4" });
}

const expanded = ref(["documents", "vuetify", "src", "material", "material-src"]);
</script>

<template>
  <TreeView
    v-slot="{ flattenItems }"
    v-model:expanded="expanded"
    guideline="straight"
    :items
    :get-key="(item) => item.id"
    class="w-full max-w-sm rounded-lg border p-2"
  >
    <TreeViewItem
      v-for="item in flattenItems"
      :key="item._id"
      v-slot="slotProps"
      v-bind="item.bind"
      class="flex min-h-8 items-center gap-2 rounded-sm px-2 text-xs outline-none hover:bg-accent/30 data-selected:bg-accent cursor-default select-none"
    >
      <TreeViewIndicator
        v-bind="slotProps"
        :has-children="item.hasChildren"
        class="inline-flex size-4 shrink-0 items-center justify-center text-muted-foreground"
      />
      <component v-if="item.hasChildren === false" :is="getFileIcon(item.value.title)" />
      {{ item.value.title }}
    </TreeViewItem>
  </TreeView>
</template>
    

Features

  • Reka UI Tree — Built on the alpha Tree primitive (TreeRoot, TreeItem, TreeVirtualizer)
  • Composable parts — Style each row with TreeViewItem, optional TreeViewIndicator, and your own label content
  • Guideline variants — Connect branches with guideline="straight" or guideline="rounded" on TreeView
  • Virtualization — Use TreeViewVirtualizer for large trees
  • Controlled expansion — Bind v-model:expanded to open and close branches programmatically

Installation

Install from the Vuzeno registry with the shadcn-vue CLI:

      
      bunx --bun shadcn-vue@latest add https://vuzeno.com/r/tree-view.json
    

Anatomy

      
      <template>
  <TreeView v-slot="{ flattenItems }" :items :get-key="(item) => item.id">
    <TreeViewItem
      v-for="item in flattenItems"
      :key="item._id"
      v-slot="slotProps"
      v-bind="item.bind"
    >
      <TreeViewIndicator v-bind="slotProps" :has-children="item.hasChildren" />
      {{ item.value.title }}
    </TreeViewItem>
  </TreeView>
</template>
    
  • TreeView — Wraps TreeRoot. Forwards all Reka Tree props and exposes the flattenItems slot prop.
  • TreeViewItem — Wraps TreeItem. Pass v-bind="item.bind" from flattenItems. Exposes isExpanded, isSelected, isIndeterminate, handleToggle, and handleSelect on the default slot.
  • TreeViewIndicator — Optional expand control for branch nodes. Pass slot props from TreeViewItem plus has-children from the flattened item.
  • TreeViewVirtualizer — Wraps TreeVirtualizer for virtualized lists. Place inside TreeView instead of iterating flattenItems manually.

Examples

Variants

Set guideline on TreeView to draw connector lines between branches. Use "straight" for vertical guides or "rounded" for elbow connectors on child rows. Omit the prop (or use "none") for a plain list.

      
      <script setup lang="ts">
import { TreeView, TreeViewIndicator, TreeViewItem } from "@/components/tree-view";

type TreeNode = {
  id: string;
  title: string;
  children?: TreeNode[];
};

const items: TreeNode[] = [
  {
    id: "src",
    title: "src",
    children: [
      {
        id: "components",
        title: "components",
        children: [
          { id: "button", title: "Button.vue" },
          { id: "input", title: "Input.vue" },
        ],
      },
      { id: "app", title: "App.vue" },
    ],
  },
  { id: "package.json", title: "package.json" },
];

const defaultExpanded = ["src", "components"];

const itemClass = "flex min-h-8 items-center gap-2 rounded-sm px-2 text-xs outline-none hover:bg-accent/30 cursor-default select-none";
const indicatorClass = "inline-flex size-4 shrink-0 items-center justify-center text-muted-foreground";
</script>

<template>
  <div class="grid w-full max-w-2xl gap-6 sm:grid-cols-2">
    <div class="space-y-2">
      <p class="text-xs font-medium text-muted-foreground">Straight</p>
      <TreeView
        v-slot="{ flattenItems }"
        guideline="straight"
        :items
        :get-key="(item) => item.id"
        :default-expanded="defaultExpanded"
        class="rounded-lg border p-2"
      >
        <TreeViewItem
          v-for="item in flattenItems"
          :key="item._id"
          v-slot="slotProps"
          v-bind="item.bind"
          :class="itemClass"
        >
          <TreeViewIndicator
            v-bind="slotProps"
            :has-children="item.hasChildren"
            :class="indicatorClass"
          />
          {{ item.value.title }}
        </TreeViewItem>
      </TreeView>
    </div>

    <div class="space-y-2">
      <p class="text-xs font-medium text-muted-foreground">Rounded</p>
      <TreeView
        v-slot="{ flattenItems }"
        guideline="rounded"
        :items
        :get-key="(item) => item.id"
        :default-expanded="defaultExpanded"
        class="rounded-lg border p-2"
      >
        <TreeViewItem
          v-for="item in flattenItems"
          :key="item._id"
          v-slot="slotProps"
          v-bind="item.bind"
          :class="itemClass"
        >
          <TreeViewIndicator
            v-bind="slotProps"
            :has-children="item.hasChildren"
            :class="indicatorClass"
          />
          {{ item.value.title }}
        </TreeViewItem>
      </TreeView>
    </div>
  </div>
</template>
    
      
      <template>
  <TreeView
    v-slot="{ flattenItems }"
    guideline="rounded"
    :items
    :get-key="(item) => item.id"
  >
    <TreeViewItem
      v-for="item in flattenItems"
      :key="item._id"
      v-slot="slotProps"
      v-bind="item.bind"
    >
      <TreeViewIndicator v-bind="slotProps" :has-children="item.hasChildren" />
      {{ item.value.title }}
    </TreeViewItem>
  </TreeView>
</template>
    
ValueDescription
noneDefault. No connector lines.
straightVertical line through expanded branch children.
roundedVertical line plus rounded elbows on child rows.

Virtual list

For large trees, swap the flattenItems loop for TreeViewVirtualizer. Give the virtualizer a fixed height and pass text-content so type-ahead search can resolve item labels.

      
      <script setup lang="ts">
import { TreeView, TreeViewIndicator, TreeViewItem, TreeViewVirtualizer } from "@/components/tree-view";

type TreeNode = {
  id: string;
  title: string;
  children?: TreeNode[];
};

function buildFiles(prefix: string, count: number): TreeNode[] {
  return Array.from({ length: count }, (_, index) => ({
    id: `${prefix}-${index}`,
    title: `${prefix}-${index + 1}.ts`,
  }));
}

const items: TreeNode[] = [
  {
    id: "src",
    title: "src",
    children: buildFiles("src", 250),
  },
  {
    id: "lib",
    title: "lib",
    children: buildFiles("lib", 150),
  },
];

const defaultExpanded = ["src", "lib"];

const itemClass = "flex min-h-8 items-center gap-2 rounded-sm px-2 text-xs outline-none hover:bg-accent/30 cursor-default select-none";
const indicatorClass = "inline-flex size-4 shrink-0 items-center justify-center text-muted-foreground";
</script>

<template>
  <TreeView
    :items
    :get-key="(item) => item.id"
    :default-expanded="defaultExpanded"
    class="w-full max-w-sm rounded-lg border p-2 h-96 overflow-y-auto"
  >
    <TreeViewVirtualizer
      v-slot="{ item }"
      :text-content="(node) => node.title"
      :estimate-size="20"
      class="overflow-y-auto"
    >
      <TreeViewItem
        v-slot="slotProps"
        v-bind="item.bind"
        :class="itemClass"
      >
        <TreeViewIndicator
          v-bind="slotProps"
          :has-children="item.hasChildren"
          :class="indicatorClass"
        />
        {{ item.value.title }}
      </TreeViewItem>
    </TreeViewVirtualizer>
  </TreeView>
</template>
    
      
      <template>
  <TreeView :items :get-key="(item) => item.id">
    <TreeViewVirtualizer
      v-slot="{ item }"
      :text-content="(node) => node.title"
      class="h-64"
    >
      <TreeViewItem v-slot="slotProps" v-bind="item.bind">
        <TreeViewIndicator v-bind="slotProps" :has-children="item.hasChildren" />
        {{ item.value.title }}
      </TreeViewItem>
    </TreeViewVirtualizer>
  </TreeView>
</template>