Skip to content

Tree Structure

Vue DnD Kit allows you to create and manage complex tree structures with full drag and drop support. You can reorder nodes, move them between branches, and nest them at different levels.

Overview

Vue DnD Kit provides powerful features for working with tree structures:

  1. Nested droppable zones: Create infinitely nested draggable areas
  2. Recursive components: Build complex hierarchical structures
  3. Automatic data management: Handle tree data updates automatically
  4. Seamless transfers: Move nodes between any level in the tree
Draggable - 7a0086e3-c7d2-4e1a-86f4-bc5de26760d1
Draggable - 70c97f7d-f0bb-4cbd-8c39-2ab6f7ed3e71
Draggable - 79a30a4a-1a50-43aa-bb3b-341b48704864
Draggable - 8fdcd569-48b5-49b3-bd5f-9ad5f0dfc7c8
Draggable - 1a4784ac-4ab1-439c-a28f-e30761ea42db
Draggable - 00f180ec-6100-4873-a15b-36709689493b
Draggable - 3755b36b-a7c4-4a18-9c9e-c2a102533506
Draggable - 96f0b3e3-01a8-473b-beaa-babe8e358a31
Draggable - 3dc10605-fffa-49fe-bd28-e6e3f65b745a
Draggable - a7515e9b-0c78-4e4e-90db-8d1089203f9a
Draggable - 0d80b56e-cb6b-4099-8841-c74d12e8ebd0

Implementation

Tree Component

The main recursive component that renders the tree structure:

vue
<script setup lang="ts">
  import Draggable from './Draggable.vue';
  import Droppable from './Droppable.vue';

  interface TreeItem {
    id?: string | number;
    children?: TreeItem[];
  }

  const { source } = defineProps<{
    source: TreeItem[];
  }>();
</script>

<template>
  <Droppable :source="source">
    <TransitionGroup name="list">
      <Draggable
        v-for="(item, index) in source"
        :key="item.id"
        :index="index"
        :source="source"
      >
        Draggable - {{ item.id }}
        <Tree
          v-if="item.children"
          :source="item.children"
        />
      </Draggable>
    </TransitionGroup>
  </Droppable>
</template>

<style scoped>
  .list-move {
    transition: all 0.3s ease;
  }
  .list-enter-active,
  .list-leave-active {
    transition: all 0.3s ease;
  }
  .list-enter-from,
  .list-leave-to {
    opacity: 0;
  }
  .list-leave-active {
    position: absolute;
  }
</style>

Draggable Component

Create draggable nodes for the tree:

vue
<script setup lang="ts">
  import { useDraggable } from '@vue-dnd-kit/core';
  import { computed } from 'vue';

  const { index, source } = defineProps<{
    index: number;
    source: any[];
  }>();

  // Using computed for index and source ensures reactivity
  // This is especially important when working with nested trees
  const { elementRef, handleDragStart, isDragging, isOvered } = useDraggable({
    data: computed(() => ({
      source,
      index,
    })),
  });
</script>

<template>
  <div
    class="draggable"
    ref="elementRef"
    :class="{ 'is-dragging': isDragging, 'is-overed': isOvered }"
  >
    <div>
      <button
        class="drag-handle"
        aria-label="Drag handle"
        @pointerdown="handleDragStart"
      >
        ⋮⋮
      </button>
      <slot />
    </div>
  </div>
</template>

<style scoped>
  .draggable {
    padding: 10px 16px;
    border: 1px solid rgba(62, 175, 124, 0.3);
    border-radius: 6px;
    background-color: rgba(62, 175, 124, 0.1);
    font-weight: 500;
    font-size: 14px;
    width: 100%;
    transition: all 0.2s ease;
  }

  .is-dragging {
    opacity: 0.5;
  }

  .is-overed {
    background-color: rgba(62, 175, 124, 0.2);
    border-color: rgba(62, 175, 124, 0.3);
  }
</style>

Droppable Component

Create drop zones for tree nodes:

vue
<script setup lang="ts">
  import { useDroppable, DnDOperations } from '@vue-dnd-kit/core';
  import { computed } from 'vue';

  const { source } = defineProps<{
    source: any[];
  }>();

  // Using computed to ensure reactivity with nested structures
  const { elementRef, isOvered } = useDroppable({
    data: computed(() => ({
      source,
    })),
    events: {
      onDrop: DnDOperations.applyTransfer,
    },
  });
</script>

<template>
  <div
    class="droppable"
    ref="elementRef"
    :class="{ 'is-overed': isOvered }"
  >
    <slot />
  </div>
</template>

<style scoped>
  .droppable {
    padding: 16px;
    border-radius: 6px;
    border: 1px dashed rgba(62, 175, 124, 0.3);
    background-color: rgba(62, 175, 124, 0.1);
    display: flex;
    flex-direction: column;
    gap: 10px;
    position: relative;
  }

  .is-overed {
    background-color: rgba(62, 175, 124, 0.2);
  }
</style>

Example Usage

Setup a tree with nested nodes:

vue
<script setup lang="ts">
  import Tree from './Tree.vue';
  import { ref } from 'vue';

  interface TreeItem {
    id: string | number;
    children?: TreeItem[];
  }

  const source = ref<TreeItem[]>([
    {
      id: crypto.randomUUID(),
      children: [
        {
          id: crypto.randomUUID(),
          children: [
            {
              id: crypto.randomUUID(),
              children: [],
            },
            {
              id: crypto.randomUUID(),
              children: [],
            },
            {
              id: crypto.randomUUID(),
              children: [],
            },
          ],
        },
      ],
    },
    {
      id: crypto.randomUUID(),
      children: [
        {
          id: crypto.randomUUID(),
          children: [
            {
              id: crypto.randomUUID(),
              children: [],
            },
          ],
        },
      ],
    },
    {
      id: crypto.randomUUID(),
      children: [
        {
          id: crypto.randomUUID(),
          children: [],
        },
        {
          id: crypto.randomUUID(),
          children: [],
        },
      ],
    },
  ]);
</script>

<template>
  <Tree :source="source" />
</template>

Key Features

Recursive Structure

The Tree component recursively renders itself for children nodes, creating an infinitely nestable structure:

vue
<Tree v-if="item.children" :source="item.children" />

Transitions for Smooth Animations

Adding transition groups enhances the user experience with smooth animations:

vue
<TransitionGroup name="list">
  <Draggable v-for="..." :key="...">
    <!-- content -->
  </Draggable>
</TransitionGroup>

Drag Handle for Better Control

Adding a dedicated drag handle improves the user interaction:

vue
<button
  class="drag-handle"
  aria-label="Drag handle"
  @pointerdown="handleDragStart"
>
  ⋮⋮
</button>

Visual Feedback

Providing visual feedback when dragging or hovering over elements:

vue
<div :class="{ 'is-dragging': isDragging, 'is-overed': isOvered }">
  <!-- content -->
</div>

Consistent Styling

Using semi-transparent colors maintains a consistent look across themes:

css
.draggable {
  border: 1px solid rgba(62, 175, 124, 0.3);
  background-color: rgba(62, 175, 124, 0.1);
}

.droppable {
  border: 1px dashed rgba(62, 175, 124, 0.3);
  background-color: rgba(62, 175, 124, 0.1);
}

.is-overed {
  background-color: rgba(62, 175, 124, 0.2);
}

Automatic Data Transfer

Vue DnD Kit automatically handles the data updates when items are moved within the tree:

js
const { elementRef } = useDroppable({
  data: computed(() => ({
    source,
  })),
  events: {
    onDrop: DnDOperations.applyTransfer, // Automatically updates tree data
  },
});

Best Practices

  1. Unique IDs: Always provide unique IDs for tree nodes to ensure proper updates
  2. Computed Properties: Use computed properties for data to maintain reactivity in nested structures
  3. Performance Optimization: Consider implementing virtualization for large trees
  4. Deep Cloning: When manually manipulating the tree data, use deep cloning to preserve reactivity
  5. Type Definitions: Use TypeScript interfaces to define your tree structure for better code maintenance
  6. Empty Children Arrays: Use empty arrays instead of undefined for leaf nodes to simplify operations
  7. Visual Feedback: Provide clear visual cues for drag states to enhance user experience
  8. Consistent Styling: Use semi-transparent colors for better theme compatibility

Released under the MIT License.