Drag
Drag defines an area from which typed data can be dragged.
Events
| Event | Description |
|---|---|
dragstart | Emitted when pointer movement exceeds delta after the gesture is initialized. |
dragend | Emitted when the operation succeeds, fails, is cancelled, or the source unmounts. Inspect event.success for the outcome. |
copy | Emitted after a successful drop onto a target whose mode is copy. |
cut | Emitted after a successful drop onto a target whose mode is cut. Use this event to remove or update source data; the library does not mutate it automatically. |
These events receive a DnDEventPayload.
Props
| Prop | Type / Default | Description |
|---|---|---|
tag | String or component ('div') | Root element or Vue component. A component must render one HTML root element; its props, attributes, listeners, and slots are forwarded. |
type | String, Number, or null (null) | Optional category used by targets to decide whether they participate. |
data | Unknown (null) | Data included in drag-and-drop event payloads. |
drag-image-opacity | Number (0.7) | Opacity applied to the source drag image. |
disabled | Boolean (false) | Prevents this component from beginning a drag. |
go-back | Boolean (false) | Animates an unsuccessful drag image back to its source. |
handle | String, Function, or null (null) | CSS selector matched inside the root, or a function returning an Element anywhere in the document. |
delta | Number (0px) | Pointer distance that must be exceeded before dragging begins. At 0, dragging begins on the first movement. |
delay | Number (0ms) | Time the pointer must remain down before the gesture initializes. Moving beyond delta before the delay finishes cancels that attempt. |
drag-class | String or null (null) | Additional class applied to drag images created by this source. |
vibration | Number (0ms) | Vibration duration when a gesture initializes on supported devices. 0 disables vibration. |
scrolling-edge-size | Number (100px) | Distance from a scroll-container edge that activates autoscroll. 0 disables source autoscroll. |
scrolling-speed | Number (50px) | Maximum pixels applied by each autoscroll step. |
scrolling-propagation | Boolean (true) | Whether autoscroll may continue through outer scroll containers. |
Slots
| Slot | Props | Description |
|---|---|---|
default | Props forwarded by a component passed to tag | Content rendered inside the draggable root. |
drag-image | None | Optional source drag-image model. See Drag images. |
Other named slots are forwarded when tag is a Vue component.
Return unsuccessful drags
Set go-back to animate the active image back to the source when a drag is released without a permitted target or is cancelled.
Release outside the target to see the drag image return to its source.
View example code
Template
<div class="dnd-demo__grid">
<div>
<span class="dnd-demo__label">Source</span>
<Drag
class="dnd-demo__item"
:data="'Example item'"
go-back
>
Drag me
</Drag>
</div>
<Drop class="dnd-demo__zone" @drop="onDrop">
<span class="dnd-demo__label">Valid target</span>
Drop here
</Drop>
</div>TypeScript
import { ref } from 'vue';
import { Drag, Drop } from 'vue-easy-dnd';
import type { DnDEventPayload } from 'vue-easy-dnd';
const status = ref('Nothing dropped yet');
const onDrop = (event: DnDEventPayload) => {
status.value = `Dropped: ${String(event.data)}`;
};Lazy and external handles
A string handle selector is matched when pointer input begins, so matching content can be rendered after Drag mounts. Only the handle or one of its descendants can initiate the drag.
For a handle outside the Drag root, pass a function returning the current handle element. The function is resolved on every pointer-down, so it can safely return a template ref that changes over time.
Select a card, then use the shared toolbar button to drag it. The handle is resolved only when the pointer goes down.
View example code
Template
<div class="external-handle-demo__toolbar">
<button
v-if="selectedId !== null"
ref="toolbarHandle"
type="button"
>
Drag selected card
</button>
<span>{{ selectedId === null ? 'Select a card first' : `Selected card ${selectedId}` }}</span>
</div>
<div class="dnd-demo__grid external-handle-demo__layout">
<div class="external-handle-demo__cards">
<Drag
v-for="item in items"
:key="item.id"
:data="item"
:handle="handles[item.id]"
type="external-handle-card"
class="dnd-demo__card external-handle-demo__card"
:class="{ 'external-handle-demo__card--selected': selectedId === item.id }"
@click="selectedId = item.id"
>
<div>
<strong>{{ item.title }}</strong>
<small>Click to select</small>
</div>
</Drag>
</div>
<Drop
accepts-type="external-handle-card"
class="dnd-demo__zone external-handle-demo__target"
@drop="onDrop"
>
{{ result }}
</Drop>
</div>TypeScript
import { ref } from 'vue';
import { Drag, Drop } from 'vue-easy-dnd';
import type { DnDEventPayload } from 'vue-easy-dnd';
const items = [
{ id: 1, title: 'Design brief' },
{ id: 2, title: 'Research notes' },
{ id: 3, title: 'Launch checklist' }
];
const selectedId = ref<number | null>(null);
const toolbarHandle = ref<HTMLButtonElement | null>(null);
const result = ref('Drop the selected card here');
const handles: Record<number, () => Element | null> = Object.fromEntries(
items.map(item => [item.id, () => selectedId.value === item.id ? toolbarHandle.value : null])
);
const onDrop = (event: DnDEventPayload) => {
const item = event.data as { title: string };
result.value = `Dropped: ${item.title}`;
};
const reset = () => {
selectedId.value = null;
result.value = 'Drop the selected card here';
};Automatic scrolling
Use scrolling-edge-size to control how close the pointer must be to an edge, scrolling-speed to control the per-step scroll delta, and scrolling-propagation to decide whether scrolling may continue through outer containers.
An active DropList can override edge size and propagation with its own scrolling-edge-size and scrolling-propagation props. Scroll speed always comes from the source Drag.
Adjust the edge activation distance and scroll delta, then choose whether scrolling can continue into the outer container.
Outer scroll area above the list
Outer scroll area below the list
View example code
Template
<div class="auto-scroll-demo__controls">
<label>
Edge activation distance: <strong>{{ edgeSize }}px</strong>
<input
v-model.number="edgeSize"
type="range"
min="20"
max="140"
step="10"
/>
</label>
<label>
Scroll delta: <strong>{{ scrollDelta }}px per step</strong>
<input
v-model.number="scrollDelta"
type="range"
min="2"
max="80"
step="2"
/>
</label>
<label class="auto-scroll-demo__toggle">
<input v-model="propagate" type="checkbox" />
Allow propagation to the outer container
</label>
</div>
<div class="auto-scroll-demo__outer">
<p class="auto-scroll-demo__spacer">
Outer scroll area above the list
</p>
<DropList
:items="items"
:scrolling-edge-size="edgeSize"
:scrolling-propagation="propagate"
class="dnd-demo__list auto-scroll-demo__inner"
column
no-animations
@reorder="$event.apply(items)"
>
<template #item="{ item }">
<Drag
:key="item"
:data="item"
:scrolling-edge-size="edgeSize"
:scrolling-speed="scrollDelta"
:scrolling-propagation="propagate"
class="dnd-demo__item auto-scroll-demo__item"
>
{{ item }}
</Drag>
</template>
<template #feedback>
<div key="feedback" class="dnd-demo__feedback" />
</template>
</DropList>
<p class="auto-scroll-demo__spacer">
Outer scroll area below the list
</p>
</div>TypeScript
import { ref } from 'vue';
import { Drag, DropList } from 'vue-easy-dnd';
const makeItems = () => Array.from({ length: 18 }, (_, index) => `Scrollable item ${index + 1}`);
const edgeSize = ref(50);
const scrollDelta = ref(12);
const propagate = ref(false);
const items = ref(makeItems());
const reset = () => {
edgeSize.value = 50;
scrollDelta.value = 12;
propagate.value = false;
items.value = makeItems();
};CSS classes
| Class | Applied when |
|---|---|
dnd-drag | Always on the Drag root. |
drag-source | This component is the source of the active operation. |
drag-mode-copy | Its active permitted target uses copy mode. |
drag-mode-cut | Its active permitted target uses cut mode. |
drag-mode-reordering | It is being reordered within its current DropList. |
drag-no-handle | No handle prop is configured. |
dnd-ghost | On drag-image clones created by Drag, Drop, or DropList. |
drag-in-progress | On the document <html> element while an operation is active. |
drag-class is applied to images produced by the source Drag. A custom image produced by a target Drop or DropList receives dnd-ghost, but not the source's drag-class.
Add dnd-no-drag to a child element to prevent gestures starting from that child or its descendants.
Types
A drag type is a string, number, or null category assigned through Drag.type. Targets use accepts-type to decide whether they participate. A target can accept one type, an array of types, or a predicate.
This filtering is separate from accepts-data: type acceptance decides whether the target participates, while data acceptance decides whether the active value may be dropped.
Each target participates only when its configured type is being dragged.
View example code
Template
<div class="dnd-demo__row">
<Drag
class="dnd-demo__item"
type="number"
:data="42"
>
Number 42
</Drag>
<Drag
class="dnd-demo__item"
type="letter"
data="A"
>
Letter A
</Drag>
</div>
<div class="dnd-demo__grid" style="margin-top: 1rem">
<Drop
class="dnd-demo__zone"
accepts-type="number"
@drop="record('Numbers', $event)"
>
<span class="dnd-demo__label">Numbers only</span>
</Drop>
<Drop
class="dnd-demo__zone"
accepts-type="letter"
@drop="record('Letters', $event)"
>
<span class="dnd-demo__label">Letters only</span>
</Drop>
</div>TypeScript
import { ref } from 'vue';
import { Drag, Drop } from 'vue-easy-dnd';
import type { DnDEventPayload } from 'vue-easy-dnd';
const lastDrop = ref('Try either item');
const record = (target: string, event: DnDEventPayload) => {
lastDrop.value = `${target} accepted ${String(event.data)}`;
};Drag images
During a drag, Vue-Easy-DnD positions an image in viewport coordinates above the page content.
The source Drag controls the initial image with its drag-image slot:
- Without the slot, the
Dragroot is cloned. - With an empty slot, no visible image is rendered.
- With slot content, that content is cloned.
Drop and DropList also provide a drag-image slot with data and type props. When an accepting target becomes active:
- Without a target slot, the source image remains active.
- With an empty target slot, no visible image is rendered over that target.
- With target slot content, that content replaces the source image.
DropList additionally provides reordering-drag-image, with the item being reordered as its item prop.
Use CSS transform on custom drag-image content to adjust its position relative to the pointer.
Dynamic drag images
Drag images are DOM clones and do not update automatically when their Vue slot model changes. After updating reactive content used by the active image, call refreshDragImage(). It waits for Vue's next render and replaces the current clone.
import { refreshDragImage } from 'vue-easy-dnd'
previewMode.value = 'expanded'
void refreshDragImage()The returned promise resolves to the new image element or null if no operation remains active.
Move 180px beyond any edge of the original source card to replace the compact preview with its expanded state.
View example code
Template
<div class="dnd-demo__grid dynamic-image-demo">
<div>
<span class="dnd-demo__label">Source</span>
<Drag
class="dnd-demo__card dynamic-image-demo__source"
type="dynamic-preview"
:data="itemName"
@dragstart="onDragStart"
@dragend="onDragEnd"
>
<strong>{{ itemName }}</strong>
<small>Move 180px beyond any edge to expand</small>
<template #drag-image>
<div class="dnd-demo__ghost dynamic-image-demo__preview" :class="previewMode">
<strong>{{ itemName }}</strong>
<small v-if="previewMode === 'expanded'">
Additional content rendered during this drag
</small>
</div>
</template>
</Drag>
</div>
<Drop
class="dnd-demo__zone dynamic-image-demo__target"
accepts-type="dynamic-preview"
@drop="onDrop"
>
<span class="dnd-demo__label">Target</span>
{{ result }}
</Drop>
</div>TypeScript
import { ref, watch } from 'vue';
import { Drag, Drop, refreshDragImage, useDragAware } from 'vue-easy-dnd';
import type { DnDEventPayload } from 'vue-easy-dnd';
const itemName = 'Demo item';
const { dragPosition } = useDragAware();
const expansionDistance = 180;
const previewMode = ref<'compact' | 'expanded'>('compact');
const sourceBounds = ref<Pick<DOMRect, 'left' | 'right' | 'top' | 'bottom'> | null>(null);
const ownsDrag = ref(false);
const result = ref('Drop the item here');
watch(dragPosition, position => {
const bounds = sourceBounds.value;
if (!ownsDrag.value || !position || !bounds) return;
const isOutsideSource = position.x < bounds.left - expansionDistance ||
position.x > bounds.right + expansionDistance ||
position.y < bounds.top - expansionDistance ||
position.y > bounds.bottom + expansionDistance;
const nextMode = isOutsideSource ? 'expanded' : 'compact';
if (nextMode === previewMode.value) return;
previewMode.value = nextMode;
void refreshDragImage();
});
const onDragStart = (event: DnDEventPayload) => {
ownsDrag.value = true;
const bounds = event.sourceController?.getElement().getBoundingClientRect();
sourceBounds.value = bounds
? { left: bounds.left, right: bounds.right, top: bounds.top, bottom: bounds.bottom }
: null;
};
const onDragEnd = () => {
ownsDrag.value = false;
sourceBounds.value = null;
previewMode.value = 'compact';
};
const onDrop = (event: DnDEventPayload) => {
result.value = `Received: ${String(event.data)}`;
};
const reset = () => {
sourceBounds.value = null;
previewMode.value = 'compact';
result.value = 'Drop the item here';
};Source and target drag images
This example combines a custom source image, images supplied by nested targets, and a DropMask:
Drag a profile icon and see how each drop zone can show a different drag image.
View example code
Template
<Drag
class="dnd-demo__item"
type="sample"
:data="profileName"
>
<img
:src="avatarAlex"
draggable="false"
:alt="`Profile icon for ${profileName}`"
class="demo-profile-icon"
/>
<template #drag-image>
<img
:src="avatarAlex"
draggable="false"
:alt="`Profile icon for ${profileName}`"
class="dnd-demo__ghost demo-drag-image"
/>
</template>
</Drag>
<Drop
class="dnd-demo__zone demo-nested-zone"
accepts-type="sample"
@drop="record('outer')"
>
<span class="dnd-demo__label">Outer target</span>
<template #drag-image="{ data }">
<img
:src="avatarJordan"
draggable="false"
:alt="`Outer drag target preview for ${data}`"
class="dnd-demo__ghost demo-drag-image"
/>
</template>
<Drop
class="dnd-demo__zone"
accepts-type="sample"
@drop="record('inner')"
>
<span class="dnd-demo__label">Nested target</span>
<DropMask class="demo-mask">
Masked area
</DropMask>
<template #drag-image="{ data }">
<img
:src="avatarSam"
draggable="false"
:alt="`Nested drag target preview for ${data}`"
class="dnd-demo__ghost demo-drag-image"
/>
</template>
</Drop>
</Drop>TypeScript
import { ref } from 'vue';
import { Drag, Drop, DropMask } from 'vue-easy-dnd';
import avatarAlex from './assets/avatar-alex.jpg';
import avatarJordan from './assets/avatar-jordan.jpg';
import avatarSam from './assets/avatar-sam.jpg';
const status = ref('Drag the item through both target levels');
const profileName = 'Profile';
const record = (target: string) => {
status.value = `Dropped on the ${target} target`;
};