Drag Animation
Animate the drag preview and list items during drag-and-drop using motion-v.
Demo
Drag any card to reorder the list. The preview pops in with a spring bounce; items slide smoothly into their new positions on drop.
How it works
The example combines three animation layers:
- Preview pop-in/out —
AnimatePresencefrommotion-vwrapsDragPreview, enabling spring animations both on mount (drag start) and unmount (drag end) via:exit. - Ghost state — the original card turns fully transparent (
opacity: 0) while dragging, hiding the "source" behind the floating preview. - List reorder — Vue's
<TransitionGroup>moves remaining cards into their new positions with a smoothtransformtransition on drop.
Custom preview with spring animation
The key to animating both drag start and drag end is wrapping DragPreview in AnimatePresence. This allows motion-v to intercept the unmount (when dragging ends) and play the :exit animation before the element is removed from the DOM.
<!-- AnimatedPreview.vue -->
<script setup lang="ts">
import { motion, AnimatePresence } from 'motion-v';
import { DragPreview } from '@vue-dnd-kit/core';
</script>
<template>
<AnimatePresence mode="popLayout">
<DragPreview v-slot="{ draggingMap }">
<motion.div
v-for="[node, draggable] in draggingMap"
:key="node"
class="preview-wrapper"
:initial="{ scale: 0.9, opacity: 0, rotate: -2 }"
:animate="{ scale: 1.06, opacity: 1, rotate: 1.5 }"
:exit="{ scale: 0.9, opacity: 0, rotate: -2 }"
:transition="{ type: 'spring', stiffness: 480, damping: 26 }"
>
<component v-if="draggable.render" :is="draggable.render" />
<component
v-else
:is="node.tagName"
v-html="draggable.initialOuterHTML"
:style="{
width: draggable.initialRect.width + 'px',
height: draggable.initialRect.height + 'px',
}"
/>
</motion.div>
</DragPreview>
</AnimatePresence>
</template>
<style scoped>
.preview-wrapper {
filter: drop-shadow(0 16px 40px rgba(0, 0, 0, 0.22));
}
</style>Why AnimatePresence with mode="popLayout"?
DragPreview internally uses v-if — when dragging ends, it unmounts. Without AnimatePresence, the :exit animation never plays because the element is removed immediately. mode="popLayout" also takes the exiting element out of flow so the list below can animate freely at the same time.
Why v-for directly on motion.div?
draggingMap is a Map<HTMLElement, DraggableEntity>. Iterating with v-for on the animated element (rather than a wrapper) lets motion-v track each draggable item by its :key independently — important for multi-drag where several items may animate in at once.
Register it via the #preview slot on DnDProvider:
<DnDProvider>
<template #preview>
<AnimatedPreview />
</template>
</DnDProvider>Ghost effect on the dragged item
Hide the source element while its preview is floating:
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDraggable } from '@vue-dnd-kit/core';
const props = defineProps<{ index: number; items: any[] }>();
const cardRef = useTemplateRef<HTMLElement>('cardRef');
const { isDragging } = makeDraggable(cardRef, {}, () => [props.index, props.items]);
</script>
<template>
<div ref="cardRef" :class="{ 'is-dragging': isDragging }">
<slot />
</div>
</template>
<style>
.is-dragging { opacity: 0; }
</style>Smooth list reordering with <TransitionGroup>
Wrap the draggable list in <TransitionGroup> and handle list-move to animate items sliding into place after a drop:
<TransitionGroup name="list">
<AnimatedCard v-for="(item, i) in items" :key="item.id" :index="i" :items="items" />
</TransitionGroup>.list-move {
transition: transform 0.28s cubic-bezier(0.165, 0.84, 0.44, 1);
}
.list-enter-active, .list-leave-active {
transition: opacity 0.2s ease, transform 0.28s cubic-bezier(0.165, 0.84, 0.44, 1);
}
.list-enter-from, .list-leave-to { opacity: 0; transform: translateY(-12px); }
.list-leave-active { position: absolute; pointer-events: none; }position: absolute on leave
list-leave-active { position: absolute } takes the leaving element out of flow so remaining items can immediately animate to their final positions without waiting for the leave transition to finish.
Drop handler
makeDroppable(
zoneRef,
{
events: {
onDrop(e) {
const r = e.helpers.suggestSort('vertical');
if (r) items.value = r.targetItems as Card[];
},
},
},
() => items.value
);See also
- DragPreview — slot API and CSS customization.
- Overlay Transition — simpler enter/leave transition using
<Transition>. - Sorting Lists — sorting without custom animations.