🎯 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
disabled
prop is false and items have unique keys - Performance issues: Use
item-key
prop 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