Both vue-draggable-next and vuedraggable are Vue components that wrap the Sortable.js library to enable drag-and-drop functionality in lists. They allow developers to reorder elements visually while keeping the underlying data array in sync. vuedraggable is the official wrapper maintained by the SortableJS organization, supporting both Vue 2 and Vue 3. vue-draggable-next is a community-driven fork created specifically to address Vue 3 compatibility during the transition period when the official package was still stabilizing its Vue 3 support.
Both vue-draggable-next and vuedraggable solve the same problem — enabling drag-and-drop lists in Vue applications by wrapping Sortable.js. While their APIs look nearly identical, the difference lies in maintenance, official backing, and long-term reliability. Let's compare how they fit into a Vue 3 architecture.
vuedraggable is the official package. It is installed directly from the main namespace.
npm install vuedraggable@next
// vuedraggable: Importing the component
import draggable from 'vuedraggable';
export default {
components: { draggable }
};
vue-draggable-next is a community fork. It requires a different package name.
npm install vue-draggable-next
// vue-draggable-next: Importing the component
import draggable from 'vue-draggable-next';
export default {
components: { draggable }
};
Both packages support Vue 3's v-model to sync the list order with your data array. However, vuedraggable has broader documentation covering this pattern.
vuedraggable uses standard Vue 3 v-model binding.
<!-- vuedraggable: Two-way binding -->
<draggable v-model="taskList" item-key="id">
<template #item="{ element }">
<div>{{ element.title }}</div>
</template>
</draggable>
vue-draggable-next implements the same v-model interface.
<!-- vue-draggable-next: Two-way binding -->
<draggable v-model="taskList" item-key="id">
<template #item="{ element }">
<div>{{ element.title }}</div>
</template>
</draggable>
Sortable.js emits events like start, end, and update. Both wrappers expose these as Vue events. You should verify event names match your version.
vuedraggable passes Sortable.js events directly to Vue listeners.
<!-- vuedraggable: Listening to events -->
<draggable
v-model="list"
@start="drag = true"
@end="drag = false"
item-key="id"
>
<!-- template content -->
</draggable>
vue-draggable-next supports the same event structure.
<!-- vue-draggable-next: Listening to events -->
<draggable
v-model="list"
@start="drag = true"
@end="drag = false"
item-key="id"
>
<!-- template content -->
</draggable>
Vue's <transition-group> works alongside these components to animate list changes. Both support this, but configuration can vary slightly depending on the Vue version.
vuedraggable integrates cleanly with Vue 3 transitions.
<!-- vuedraggable: With transitions -->
<draggable v-model="list" item-key="id"
:component-data="{ tag: 'transition-group', name: 'list' }">
<template #item="{ element }">
<div class="item">{{ element.name }}</div>
</template>
</draggable>
vue-draggable-next offers similar transition props.
<!-- vue-draggable-next: With transitions -->
<draggable v-model="list" item-key="id"
:component-data="{ tag: 'transition-group', name: 'list' }">
<template #item="{ element }">
<div class="item">{{ element.name }}</div>
</template>
</draggable>
This is the most critical difference for architectural decisions. One is official, and one is a community effort.
vuedraggable is maintained by the SortableJS organization.
// vuedraggable: Official repository
// github.com/SortableJS/Vue.Draggable
vue-draggable-next is maintained by individual contributors.
// vue-draggable-next: Community repository
// github.com/hellodongsheng/vue-draggable-next
Since both wrap the same underlying engine, they share most core features. Here are key overlaps:
// Both rely on Sortable.js core
// Configuration options like 'group', 'animation', 'ghostClass' work the same
// Both: v-model syncs the array
// list.value moves automatically after drop
<!-- Both use the #item slot -->
<template #item="{ element }">
<div>{{ element.name }}</div>
</template>
group, animation, disabled.<!-- Both: Passing Sortable options -->
<draggable :animation="200" :group="'people'" v-model="list" item-key="id" />
// Both: TypeScript support
import draggable from 'vuedraggable'; // or 'vue-draggable-next'
// Types available for props and events
| Feature | Shared by Both |
|---|---|
| Core Engine | 🧩 Sortable.js |
| Data Sync | 📋 v-model array update |
| Rendering | 🎨 Vue 3 slots |
| Configuration | ⚙️ Sortable.js props |
| Vue Version | 🌐 Vue 3 supported |
| Feature | vuedraggable | vue-draggable-next |
|---|---|---|
| Maintainer | 🏢 SortableJS Org | 👤 Community Contributor |
| Longevity | ✅ High (Official) | ⚠️ Medium (Fork) |
| Update Speed | 🚀 Fast (Synced with core) | 🐢 Variable (Depends on author) |
| Recommendation | 🟢 Default for new projects | 🟡 Legacy or specific fixes |
| Vue 2 Support | ✅ Yes (v2 package) | ❌ Vue 3 only |
vuedraggable is like the standard library 📚 — it's the official tool backed by the creators of the underlying engine. Ideal for new projects, enterprise systems, and teams that need guaranteed support.
vue-draggable-next is like a specialized patch 🔧 — created to fill a gap when the official tool wasn't ready for Vue 3. Useful for maintaining older Vue 3 projects that adopted it early, but less ideal for starting fresh.
Final Thought: Since vuedraggable now fully supports Vue 3, there is rarely a technical reason to choose the fork for new work. Stick with the official package to reduce risk and ensure compatibility with future Sortable.js updates.
Choose vue-draggable-next only if you are maintaining an existing codebase that already depends on it or if you encounter a specific bug in the official package that this fork has resolved. For greenfield projects, it is generally better to avoid community forks when an official, fully-compatible alternative exists.
Choose vuedraggable for new projects because it is the official package maintained by the SortableJS team. It guarantees long-term support, timely updates for Sortable.js features, and consistent behavior across Vue 2 and Vue 3. It is the safest bet for enterprise applications where stability and maintenance matter most.
🎯 Vue 3 drag-and-drop component based on Sortable.js
✨ Features:
📚 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>
<script setup> syntax.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>
disabled prop is false and items have unique keysitem-key prop for better trackingtag="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:
Made with ❤️ for the Vue.js community