🎯 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 build
This 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