npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vue3-drag-drop

v1.2.0

Published

Simple drag and drop using native event API for Vue 3

Readme

vue3-drag-drop

Simple, lightweight drag and drop for Vue 3 using the native HTML Drag and Drop API.

npm license

Open in StackBlitz →

[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() on dragover or drops silently fail.

This package wraps those rough edges so you can focus on your app.


Installation

npm install vue3-drag-drop

Quick 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 build

To test changes in another project locally:

# In this repo
npm link

# In your other project
npm link vue3-drag-drop

License

MIT