vue3-drag-drop
v1.2.0
Published
Simple drag and drop using native event API for Vue 3
Maintainers
Readme
vue3-drag-drop
Simple, lightweight drag and drop for Vue 3 using the native HTML Drag and Drop API.
[Live Demo →] (https://tusharjoy.github.io/vue3-drag-drop/)
Table of Contents
Why
The native HTML Drag and Drop API has a few painful quirks:
- Transfer data is not available during
dragover— you can't inspect what's being dragged to decide whether to accept it. - You must serialize complex data (objects, arrays) to strings yourself.
- Every drop target needs
event.preventDefault()ondragoveror drops silently fail.
This package wraps those rough edges so you can focus on your app.
Installation
npm install vue3-drag-dropQuick Start
Register globally in main.js:
import { createApp } from "vue";
import App from "./App.vue";
import { Drag, Drop } from "vue3-drag-drop";
const app = createApp(App);
app.component("Drag", Drag);
app.component("Drop", Drop);
app.mount("#app");Or import locally in a component:
<script setup>
import { Drag, Drop } from "vue3-drag-drop";
</script>API
Components
<Drag>
Wraps any content to make it draggable. Renders as a <div> by default (override with the tag prop).
<Drop>
A drop target. Accepts any <Drag>. Renders as a <div> by default.
Drag Props
| Prop | Type | Default | Description |
|---|---|---|---|
| draggable | Boolean | true | Toggle draggability on/off |
| transfer-data | any | null | Data passed to all Drop events |
| effect-allowed | String | null | One of none copy copyLink copyMove link linkMove move all uninitialized — see MDN |
| image | String | null | URL for custom drag image |
| image-x-offset | Number | 0 | X offset for custom drag image anchor |
| image-y-offset | Number | 0 | Y offset for custom drag image anchor |
| hide-image-html | Boolean | true | Hide off-screen image slot HTML |
| tag | String | "div" | HTML tag for the wrapper element |
Drop Props
| Prop | Type | Default | Description |
|---|---|---|---|
| tag | String | "div" | HTML tag for the wrapper element |
Events
All events receive the same two arguments:
| Argument | Type | Description |
|---|---|---|
| transferData | any | The value set on the <Drag>'s transfer-data prop |
| nativeEvent | DragEvent | The native browser event |
<Drag> events
| Event | Fired |
|---|---|
| dragstart | Once when drag begins |
| drag | Continuously while dragging |
| dragenter | When drag enters a Drop |
| dragleave | When drag leaves a Drop |
| dragend | Once when drag ends (after drop) |
<Drop> events
| Event | Fired |
|---|---|
| dragenter | When a Drag enters this target |
| dragover | Continuously while a Drag is over this target |
| dragleave | When a Drag leaves this target |
| drop | When a Drag is dropped here |
Slots
Default slot — <Drag> and <Drop>
Scoped. The scope exposes transferData — the current drag's transfer data. For <Drag>, populated while dragging. For <Drop>, populated while a drag is over it.
<Drop v-slot="{ transferData }">
<div :class="{ highlight: transferData }">
{{ transferData ? `Dropping: ${transferData.name}` : "Drop here" }}
</div>
</Drop>image slot — <Drag> only
Use HTML as a custom drag image instead of the browser default:
<Drag :transfer-data="item">
{{ item.label }}
<template v-slot:image>
<div class="drag-ghost">{{ item.label }}</div>
</template>
</Drag>The slot content is rendered off-screen (position: fixed; top: -1000px) so it's visible to the browser but not to the user. Set hide-image-html="false" to disable this behavior.
Examples
Basic drag and drop
<template>
<Drag :transfer-data="{ id: 1, label: 'Item A' }">
Drag me
</Drag>
<Drop @drop="onDrop">
Drop here
</Drop>
</template>
<script setup>
import { Drag, Drop } from "vue3-drag-drop";
function onDrop(transferData, nativeEvent) {
console.log("Dropped:", transferData); // { id: 1, label: 'Item A' }
}
</script>Transfer complex data
transfer-data accepts any JavaScript value — no serialization needed:
<template>
<Drag
v-for="card in cards"
:key="card.id"
:transfer-data="card"
>
{{ card.title }}
</Drag>
<Drop @drop="addCard">
<div v-for="card in column" :key="card.id">{{ card.title }}</div>
</Drop>
</template>
<script setup>
import { ref } from "vue";
import { Drag, Drop } from "vue3-drag-drop";
const cards = ref([
{ id: 1, title: "Task 1", priority: "high" },
{ id: 2, title: "Task 2", priority: "low" },
]);
const column = ref([]);
function addCard(card) {
column.value.push(card);
}
</script>Conditional drop acceptance
Use transferData in the Drop's scoped slot to show visual feedback — and inspect it in dragover to selectively accept drops:
<template>
<Drag :transfer-data="{ type: 'image', url: '...' }">Image file</Drag>
<Drag :transfer-data="{ type: 'video', url: '...' }">Video file</Drag>
<Drop
v-slot="{ transferData }"
@dragover="onDragOver"
@drop="onDrop"
>
<div :class="{ accepting: transferData?.type === 'image' }">
Images only
</div>
</Drop>
</template>
<script setup>
import { Drag, Drop } from "vue3-drag-drop";
function onDragOver(transferData, nativeEvent) {
if (transferData?.type !== "image") {
nativeEvent.dataTransfer.dropEffect = "none";
}
}
function onDrop(transferData, nativeEvent) {
if (transferData?.type === "image") {
console.log("Accepted image:", transferData.url);
}
}
</script>Custom drag image via slot
<template>
<Drag :transfer-data="item">
{{ item.name }}
<template v-slot:image>
<div class="custom-ghost">
📦 {{ item.name }}
</div>
</template>
</Drag>
</template>
<script setup>
import { Drag } from "vue3-drag-drop";
const item = { name: "My Item" };
</script>
<style>
.custom-ghost {
background: #4f46e5;
color: white;
padding: 8px 16px;
border-radius: 6px;
}
</style>Scoped slot — show data on hover
<template>
<Drag :transfer-data="{ name: 'Report.pdf', size: '2.4MB' }">
Report.pdf
</Drag>
<Drop v-slot="{ transferData }">
<div class="dropzone" :class="{ active: transferData }">
<span v-if="transferData">
Drop to upload {{ transferData.name }} ({{ transferData.size }})
</span>
<span v-else>Drop files here</span>
</div>
</Drop>
</template>
<script setup>
import { Drag, Drop } from "vue3-drag-drop";
</script>Tracking drag lifecycle events
Use @dragstart and @dragend on <Drag> to track when a drag begins and ends. Use @dragenter and @dragleave on <Drop> to react when the drag enters or leaves:
<template>
<Drag
:transfer-data="item"
@dragstart="onDragStart"
@dragend="onDragEnd"
>
{{ item.name }}
</Drag>
<Drop
@dragenter="onEnter"
@dragleave="onLeave"
@drop="onDrop"
>
<div :class="{ active: isOver }">Drop here</div>
</Drop>
</template>
<script setup>
import { ref } from "vue";
import { Drag, Drop } from "vue3-drag-drop";
const item = { name: "my-file.txt", size: 1024 };
const isDragging = ref(false);
const isOver = ref(false);
function onDragStart(transferData, nativeEvent) {
isDragging.value = true;
console.log("Drag started:", transferData.name);
}
function onDragEnd(transferData, nativeEvent) {
isDragging.value = false;
console.log("Drag ended");
}
function onEnter(transferData, nativeEvent) {
isOver.value = true;
}
function onLeave(transferData, nativeEvent) {
isOver.value = false;
}
function onDrop(transferData, nativeEvent) {
isOver.value = false;
console.log("Dropped:", transferData.name, transferData.size, "bytes");
}
</script>Moving and cloning between lists
Move (item leaves source list):
<template>
<div class="list">
<Drag
v-for="item in listA"
:key="item.id"
:transfer-data="{ item, sourceList: 'A' }"
>
{{ item.label }}
</Drag>
</div>
<Drop @drop="moveItem">
<div v-for="item in listB" :key="item.id">{{ item.label }}</div>
</Drop>
</template>
<script setup>
import { ref } from "vue";
import { Drag, Drop } from "vue3-drag-drop";
const listA = ref([
{ id: 1, label: "Item 1" },
{ id: 2, label: "Item 2" },
]);
const listB = ref([]);
function moveItem({ item, sourceList }) {
if (sourceList === "A") {
listA.value = listA.value.filter((i) => i.id !== item.id);
listB.value.push(item);
}
}
</script>Clone (item stays in source list):
<template>
<div class="list">
<Drag
v-for="item in listA"
:key="item.id"
:transfer-data="item"
>
{{ item.label }}
</Drag>
</div>
<Drop @drop="cloneItem">
<div v-for="item in listB" :key="item.id">{{ item.label }}</div>
</Drop>
</template>
<script setup>
import { ref } from "vue";
import { Drag, Drop } from "vue3-drag-drop";
const listA = ref([
{ id: 1, label: "Item 1" },
{ id: 2, label: "Item 2" },
]);
const listB = ref([]);
function cloneItem(item) {
if (!listB.value.find((i) => i.id === item.id)) {
listB.value.push({ ...item });
}
}
</script>Touch Support
Touch drag and drop is supported via a bundled polyfill based on DragDropTouch. No configuration needed — it activates automatically in browsers without native drag and drop touch support.
Development
# Install deps
npm install
# Start dev server (http://localhost:5173)
npm run dev
# Build library to dist/
npm run buildTo test changes in another project locally:
# In this repo
npm link
# In your other project
npm link vue3-drag-drop