🎯 Vue 3 drag-and-drop component based on Sortable.js
✨ Features:
- 🚀 Vue 3 Composition API support
- 📱 Touch-friendly (mobile support)
- 🎨 No CSS framework dependency
- 📦 TypeScript definitions included
- ⚡ Lightweight (~7kb gzipped)
- 🔧 All Sortable.js options supported
📚 Live Demo & Playground | 📖 Migration Guide | 🎯 Examples
# npm
npm install vue-draggable-next
# yarn  
yarn add vue-draggable-next
# pnpm
pnpm add vue-draggable-next<template>
  <div class="drag-container">
    <draggable 
      v-model="list" 
      group="people" 
      @change="onListChange"
      item-key="id"
    >
      <template #item="{ element }">
        <div class="drag-item">
          {{ element.name }}
        </div>
      </template>
    </draggable>
  </div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { VueDraggableNext } from 'vue-draggable-next'
// Define the item type
interface Person {
  id: number
  name: string
}
// Reactive list
const list = ref<Person[]>([
  { id: 1, name: 'John' },
  { id: 2, name: 'Jane' },
  { id: 3, name: 'Bob' }
])
// Handle changes
const onListChange = (event: any) => {
  console.log('List changed:', event)
}
</script>
<style scoped>
.drag-container {
  min-height: 200px;
  padding: 20px;
}
.drag-item {
  padding: 10px;
  margin: 5px 0;
  background: #f0f0f0;
  border-radius: 4px;
  cursor: move;
  transition: background 0.2s;
}
.drag-item:hover {
  background: #e0e0e0;
}
</style><template>
  <draggable 
    :list="list" 
    class="drag-area"
    @change="handleChange"
  >
    <div 
      v-for="element in list"
      :key="element.id"
      class="drag-item"
    >
      {{ element.name }}
    </div>
  </draggable>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { VueDraggableNext } from 'vue-draggable-next'
export default defineComponent({
  components: {
    draggable: VueDraggableNext
  },
  data() {
    return {
      list: [
        { id: 1, name: 'Item 1' },
        { id: 2, name: 'Item 2' },
        { id: 3, name: 'Item 3' }
      ]
    }
  },
  methods: {
    handleChange(event: any) {
      console.log('Changed:', event)
    }
  }
})
</script>| Prop | Type | Default | Description | 
|---|---|---|---|
| modelValue | Array | [] | Array to be synchronized with drag-and-drop (use with v-model) | 
| list | Array | [] | Alternative to modelValue, directly mutates the array | 
| itemKey | String|Function | undefined | Key to use for tracking items (recommended for better performance) | 
| tag | String | 'div' | HTML tag for the root element | 
| component | String | null | Vue component name to use as root element | 
| componentData | Object | null | Props/attrs to pass to the component | 
| clone | Function | (item) => item | Function to clone items when dragging | 
| move | Function | null | Function to control move operations | 
| group | String|Object | undefined | Sortable group options | 
| sort | Boolean | true | Enable sorting within the list | 
| disabled | Boolean | false | Disable drag and drop | 
| animation | Number | 0 | Animation speed (ms) | 
| ghostClass | String | '' | CSS class for the ghost element | 
| chosenClass | String | '' | CSS class for the chosen element | 
| dragClass | String | '' | CSS class for the dragging element | 
| Event | Description | Payload | 
|---|---|---|
| @change | Fired when the list changes | { added?, removed?, moved? } | 
| @start | Dragging started | SortableEvent | 
| @end | Dragging ended | SortableEvent | 
| @add | Item added from another list | SortableEvent | 
| @remove | Item removed to another list | SortableEvent | 
| @update | Item order changed | SortableEvent | 
| @sort | Any change to the list | SortableEvent | 
| @choose | Item is chosen | SortableEvent | 
| @unchoose | Item is unchosen | SortableEvent | 
<template>
  <div class="lists-container">
    <div class="list-column">
      <h3>Todo</h3>
      <draggable 
        v-model="todoList"
        group="tasks"
        class="drag-area"
        :animation="150"
      >
        <div 
          v-for="item in todoList"
          :key="item.id"
          class="task-item"
        >
          {{ item.text }}
        </div>
      </draggable>
    </div>
    
    <div class="list-column">
      <h3>Done</h3>
      <draggable 
        v-model="doneList"
        group="tasks"
        class="drag-area"
        :animation="150"
      >
        <div 
          v-for="item in doneList"
          :key="item.id"
          class="task-item done"
        >
          {{ item.text }}
        </div>
      </draggable>
    </div>
  </div>
</template>
<script setup>
import { ref } from 'vue'
import { VueDraggableNext as draggable } from 'vue-draggable-next'
const todoList = ref([
  { id: 1, text: 'Learn Vue 3' },
  { id: 2, text: 'Build awesome apps' }
])
const doneList = ref([
  { id: 3, text: 'Read documentation' }
])
</script><template>
  <draggable 
    v-model="list"
    handle=".drag-handle"
    :animation="200"
  >
    <div 
      v-for="item in list"
      :key="item.id"
      class="item-with-handle"
    >
      <span class="drag-handle">⋮⋮</span>
      <span class="item-content">{{ item.name }}</span>
      <button @click="deleteItem(item.id)">Delete</button>
    </div>
  </draggable>
</template>
<script setup>
import { ref } from 'vue'
import { VueDraggableNext as draggable } from 'vue-draggable-next'
const list = ref([
  { id: 1, name: 'Item 1' },
  { id: 2, name: 'Item 2' }
])
const deleteItem = (id) => {
  const index = list.value.findIndex(item => item.id === id)
  if (index > -1) {
    list.value.splice(index, 1)
  }
}
</script>
<style scoped>
.item-with-handle {
  display: flex;
  align-items: center;
  padding: 10px;
  margin: 5px 0;
  background: white;
  border: 1px solid #ddd;
  border-radius: 4px;
}
.drag-handle {
  cursor: grab;
  margin-right: 10px;
  color: #999;
  user-select: none;
}
.drag-handle:active {
  cursor: grabbing;
}
.item-content {
  flex: 1;
}
</style><template>
  <draggable 
    v-model="list"
    tag="transition-group"
    :component-data="{
      tag: 'div',
      type: 'transition',
      name: 'fade'
    }"
    :animation="200"
  >
    <div 
      v-for="item in list"
      :key="item.id"
      class="fade-item"
    >
      {{ item.text }}
    </div>
  </draggable>
</template>
<script setup>
import { ref } from 'vue'
import { VueDraggableNext as draggable } from 'vue-draggable-next'
const list = ref([
  { id: 1, text: 'Smooth transition' },
  { id: 2, text: 'On drag and drop' }
])
</script>
<style scoped>
.fade-item {
  padding: 15px;
  margin: 8px 0;
  background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
  color: white;
  border-radius: 8px;
  transition: all 0.3s ease;
}
.fade-enter-active, .fade-leave-active {
  transition: all 0.3s ease;
}
.fade-enter-from, .fade-leave-to {
  opacity: 0;
  transform: translateX(30px);
}
</style>// types.ts
export interface DraggableItem {
  id: string | number
  [key: string]: any
}
export interface DragChangeEvent<T = DraggableItem> {
  added?: {
    newIndex: number
    element: T
  }
  removed?: {
    oldIndex: number
    element: T
  }
  moved?: {
    newIndex: number
    oldIndex: number
    element: T
  }
}<template>
  <draggable 
    v-model="items"
    @change="onListChange"
    item-key="id"
  >
    <template #item="{ element }: { element: TodoItem }">
      <div class="todo-item">
        <input 
          v-model="element.completed"
          type="checkbox"
        >
        <span :class="{ done: element.completed }">
          {{ element.text }}
        </span>
      </div>
    </template>
  </draggable>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { VueDraggableNext as draggable } from 'vue-draggable-next'
import type { DragChangeEvent } from './types'
interface TodoItem {
  id: number
  text: string
  completed: boolean
}
const items = ref<TodoItem[]>([
  { id: 1, text: 'Learn TypeScript', completed: false },
  { id: 2, text: 'Build Vue 3 app', completed: true }
])
const onListChange = (event: DragChangeEvent<TodoItem>) => {
  if (event.added) {
    console.log('Added item:', event.added.element)
  }
  if (event.removed) {
    console.log('Removed item:', event.removed.element)
  }
  if (event.moved) {
    console.log('Moved item:', event.moved.element)
  }
}
</script><script setup>
import { ref } from 'vue'
import { VueDraggableNext as draggable } from 'vue-draggable-next'
const sourceList = ref([
  { id: 1, name: 'Template Item', color: 'blue' }
])
const targetList = ref([])
// Deep clone function for complex objects
const cloneItem = (original) => {
  return {
    ...original,
    id: Date.now(), // Generate new ID
    name: `Copy of ${original.name}`
  }
}
</script>
<template>
  <div class="clone-demo">
    <div class="source">
      <h3>Source (Clone)</h3>
      <draggable 
        v-model="sourceList"
        :group="{ name: 'shared', pull: 'clone', put: false }"
        :clone="cloneItem"
        :sort="false"
      >
        <div v-for="item in sourceList" :key="item.id">
          {{ item.name }}
        </div>
      </draggable>
    </div>
    
    <div class="target">
      <h3>Target</h3>
      <draggable 
        v-model="targetList"
        group="shared"
      >
        <div v-for="item in targetList" :key="item.id">
          {{ item.name }}
        </div>
      </draggable>
    </div>
  </div>
</template><script setup>
import { ref } from 'vue'
import { VueDraggableNext as draggable } from 'vue-draggable-next'
const list = ref([
  { id: 1, name: 'Movable item', locked: false },
  { id: 2, name: 'Locked item', locked: true },
  { id: 3, name: 'Another movable', locked: false }
])
// Prevent moving locked items
const checkMove = (event) => {
  // Don't allow moving locked items
  if (event.draggedContext.element.locked) {
    return false
  }
  
  // Don't allow dropping on locked items
  if (event.relatedContext.element?.locked) {
    return false
  }
  
  return true
}
</script>
<template>
  <draggable 
    v-model="list"
    :move="checkMove"
  >
    <div 
      v-for="item in list"
      :key="item.id"
      :class="{ locked: item.locked }"
      class="move-item"
    >
      {{ item.name }}
      <span v-if="item.locked">🔒</span>
    </div>
  </draggable>
</template>
<style scoped>
.move-item.locked {
  opacity: 0.6;
  cursor: not-allowed;
}
</style>If you're migrating from the Vue 2 version, here are the key changes:
<draggable v-model="list" @end="onEnd">
  <div v-for="item in list" :key="item.id">
    {{ item.name }}
  </div>
</draggable><!-- Option 1: Using item-key prop (recommended) -->
<draggable v-model="list" item-key="id" @end="onEnd">
  <template #item="{ element }">
    <div>{{ element.name }}</div>
  </template>
</draggable>
<!-- Option 2: Traditional approach (still works) -->
<draggable v-model="list" @end="onEnd">
  <div v-for="item in list" :key="item.id">
    {{ item.name }}
  </div>
</draggable>- Vue 3 required: This package only works with Vue 3
- Composition API: Full support for <script setup>syntax
- TypeScript: Built-in TypeScript definitions
- Performance: Better performance with item-key prop
.ghost {
  opacity: 0.5;
  background: #c8ebfb;
  border: 2px dashed #2196f3;
}
.chosen {
  transform: rotate(5deg);
}
.drag {
  transform: rotate(0deg);
}<draggable 
  v-model="list"
  :animation="300"
  easing="cubic-bezier(0.4, 0, 0.2, 1)"
  ghost-class="ghost"
  chosen-class="chosen"
  drag-class="drag"
>
  <!-- items -->
</draggable>- Items not dragging: Check if disabledprop is false and items have unique keys
- Performance issues: Use item-keyprop for better tracking
- Touch not working: Ensure touch-action CSS is not preventing touch events
- Transitions glitching: Use tag="transition-group"with proper transition classes
<draggable 
  v-model="list"
  @start="console.log('Drag started', $event)"
  @end="console.log('Drag ended', $event)"
  @change="console.log('List changed', $event)"
>
  <!-- items -->
</draggable>The component works out of the box on mobile devices. For better mobile experience:
.drag-item {
  /* Prevent text selection during drag */
  user-select: none;
  -webkit-user-select: none;
  
  /* Better touch targets */
  min-height: 44px;
  
  /* Smooth feedback */
  transition: transform 0.2s ease;
}
.drag-item:active {
  transform: scale(1.02);
}We welcome contributions! Please see our Contributing Guide for details.
# Clone the repository
git clone https://github.com/anish2690/vue-draggable-next.git
# Install dependencies
npm install
# Run development server
npm run playground:dev
# Run tests
npm test
# Build for production
npm run buildThis project is heavily inspired by SortableJS/Vue.Draggable and built on top of SortableJS.
If this project helps you, please consider:
- ⭐ Starring the repository
- 🐛 Reporting bugs
- 💡 Suggesting features
- 🤝 Contributing code
Made with ❤️ for the Vue.js community