Skip to content

Drag

Drag defines an area from which typed data can be dragged.

Events

EventDescription
dragstartEmitted when pointer movement exceeds delta after the gesture is initialized.
dragendEmitted when the operation succeeds, fails, is cancelled, or the source unmounts. Inspect event.success for the outcome.
copyEmitted after a successful drop onto a target whose mode is copy.
cutEmitted 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

PropType / DefaultDescription
tagString or component ('div')Root element or Vue component. A component must render one HTML root element; its props, attributes, listeners, and slots are forwarded.
typeString, Number, or null (null)Optional category used by targets to decide whether they participate.
dataUnknown (null)Data included in drag-and-drop event payloads.
drag-image-opacityNumber (0.7)Opacity applied to the source drag image.
disabledBoolean (false)Prevents this component from beginning a drag.
go-backBoolean (false)Animates an unsuccessful drag image back to its source.
handleString, Function, or null (null)CSS selector matched inside the root, or a function returning an Element anywhere in the document.
deltaNumber (0px)Pointer distance that must be exceeded before dragging begins. At 0, dragging begins on the first movement.
delayNumber (0ms)Time the pointer must remain down before the gesture initializes. Moving beyond delta before the delay finishes cancels that attempt.
drag-classString or null (null)Additional class applied to drag images created by this source.
vibrationNumber (0ms)Vibration duration when a gesture initializes on supported devices. 0 disables vibration.
scrolling-edge-sizeNumber (100px)Distance from a scroll-container edge that activates autoscroll. 0 disables source autoscroll.
scrolling-speedNumber (50px)Maximum pixels applied by each autoscroll step.
scrolling-propagationBoolean (true)Whether autoscroll may continue through outer scroll containers.

Slots

SlotPropsDescription
defaultProps forwarded by a component passed to tagContent rendered inside the draggable root.
drag-imageNoneOptional 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.

Return unsuccessful drags

Release outside the target to see the drag image return to its source.

Source
Drag me
Valid target Drop here
View example code

Template

vue
<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

ts
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.

Resolve a drag handle outside the Drag component

Select a card, then use the shared toolbar button to drag it. The handle is resolved only when the pointer goes down.

Select a card first
Design briefClick to select
Research notesClick to select
Launch checklistClick to select
Drop the selected card here
View example code

Template

vue
<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

ts
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.

Control nested edge scrolling

Adjust the edge activation distance and scroll delta, then choose whether scrolling can continue into the outer container.

Outer scroll area above the list

Scrollable item 1
Scrollable item 2
Scrollable item 3
Scrollable item 4
Scrollable item 5
Scrollable item 6
Scrollable item 7
Scrollable item 8
Scrollable item 9
Scrollable item 10
Scrollable item 11
Scrollable item 12
Scrollable item 13
Scrollable item 14
Scrollable item 15
Scrollable item 16
Scrollable item 17
Scrollable item 18

Outer scroll area below the list

View example code

Template

vue
<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

ts
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

ClassApplied when
dnd-dragAlways on the Drag root.
drag-sourceThis component is the source of the active operation.
drag-mode-copyIts active permitted target uses copy mode.
drag-mode-cutIts active permitted target uses cut mode.
drag-mode-reorderingIt is being reordered within its current DropList.
drag-no-handleNo handle prop is configured.
dnd-ghostOn drag-image clones created by Drag, Drop, or DropList.
drag-in-progressOn 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.

Accept drag types

Each target participates only when its configured type is being dragged.

Number 42
Letter A
Numbers only
Letters only
View example code

Template

vue
<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

ts
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 Drag root 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.

ts
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.

Update a drag image during an active drag

Move 180px beyond any edge of the original source card to replace the compact preview with its expanded state.

Source
Demo itemMove 180px beyond any edge to expand
Target Drop the item here
View example code

Template

vue
<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

ts
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:

Custom drag images

Drag a profile icon and see how each drop zone can show a different drag image.

Profile icon for Profile
Outer target
Nested target
Masked area
View example code

Template

vue
<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

ts
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`;
};