Skip to content

Animating Drag and Drop ​

Adding animations to your drag and drop interactions can significantly enhance the user experience. Vue DnD Kit provides powerful animation capabilities to create engaging interactions.

Overview ​

Vue DnD Kit offers several ways to animate drag and drop interactions:

  1. Custom drag containers: Create specialized components for drag overlays
  2. CSS transitions: Use Vue's transition system for simple animations
  3. JavaScript animations: Integrate with libraries like GSAP for complex effects
  4. Animation state tracking: Monitor animation states to coordinate UI updates
Drag me to zone
Drop here

Custom Drag Container ​

For advanced animations, you can create a custom container component that controls how dragged elements appear:

vue
<script setup lang="ts">
  import { useDragContainer } from '@vue-dnd-kit/core';
  import { computed, CSSProperties } from 'vue';
  import gsap from 'gsap';

  const {
    elementRef,
    pointerPosition,
    isDragging,
    animating,
    draggingElements,
  } = useDragContainer({
    name: 'ripple',
    onEnter,
    onLeave,
  });

  const computedStyle = computed<CSSProperties>(() => {
    const currentX = pointerPosition.current.value?.x ?? 0;
    const currentY = pointerPosition.current.value?.y ?? 0;
    const offsetX = pointerPosition.offset.pixel.value?.x ?? 0;
    const offsetY = pointerPosition.offset.pixel.value?.y ?? 0;

    return {
      transform: `translate3d(${currentX - offsetX}px, ${
        currentY - offsetY
      }px, 0)`,
      zIndex: 1000,
      position: 'fixed',
      top: 0,
      left: 0,
      transition: '0.3s cubic-bezier(0.165, 0.84, 0.44, 1)',
    };
  });

  function onEnter(el: HTMLElement | Element) {
    gsap.fromTo(
      el,
      {
        clipPath: 'circle(100% at 50% 50%)',
      },
      {
        clipPath: 'circle(50% at 50% 50%)',
        duration: 0.7,
        ease: 'elastic.inOut(2.5, 1)',
        width: 100,
      }
    );
  }

  function onLeave(el: HTMLElement | Element, done: () => void) {
    animating.leave.value = true;
    gsap.to(el, {
      duration: 0.7,
      scaleX: 0.25,
      scaleY: 0.25,
      ease: 'elastic.inOut(2.5, 1)',
    });
    gsap.to(el, {
      duration: 0.2,
      delay: 0.5,
      scaleX: 0,
      scaleY: 0,
      opacity: 0,
      onComplete: () => {
        animating.leave.value = false;
        done();
      },
    });
  }
</script>

<template>
  <div
    v-if="isDragging"
    ref="elementRef"
    :style="computedStyle"
    class="ripple"
  >
    <div
      v-for="[, element] in draggingElements"
      :key="element.id"
      v-html="element.initialHTML"
      class="dragging-element"
      :style="{
        width: element.initialRect?.width + 'px',
        height: element.initialRect?.height + 'px',
      }"
    />
  </div>
</template>

<style scoped>
  .ripple {
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
    filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.15));
  }

  .dragging-element {
    border-radius: 8px;
    overflow: hidden;
    background-color: var(--vp-c-brand);
    color: white;
    font-weight: 500;
  }

  .ripple-enter-active,
  .ripple-leave-active {
    transition: all 0.7s cubic-bezier(0.16, 1, 0.3, 1);
  }
</style>

Draggable Component with Animation Tracking ​

This component shows how to create a draggable element that coordinates its animations with the custom container:

vue
<script setup lang="ts">
  import { useDnDStore, useDraggable } from '@vue-dnd-kit/core';
  import CustomContainer from './CustomContainer.vue';
  import { computed, markRaw } from 'vue';

  const emit = defineEmits<{
    (e: 'end'): void;
  }>();

  const { elementRef, handleDragStart, isDragging } = useDraggable({
    container: markRaw(CustomContainer),
    events: {
      onEnd: () => emit('end'),
    },
  });

  const { activeContainer } = useDnDStore();

  const containerIsAnimating = computed(
    () =>
      activeContainer.animating.leave.value ||
      isDragging.value ||
      activeContainer.ref.value
  );
</script>

<template>
  <Transition name="fill">
    <div
      v-show="!containerIsAnimating"
      ref="elementRef"
      @pointerdown="handleDragStart"
      class="fill-container"
      :class="{ 'fill-container-active': isDragging }"
    >
      <slot>Drag me</slot>
    </div>
  </Transition>
</template>

<style scoped>
  .fill-container {
    padding: 20px;
    box-sizing: border-box;
    width: 150px;
    height: 100px;
    border-radius: 8px;
    background-color: var(--vp-c-brand);
    color: white;
    font-weight: 500;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: grab;
    user-select: none;
    transition: transform 0.2s, box-shadow 0.2s;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
    clip-path: circle(150% at 50% 50%);
  }

  .fill-container:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
  }

  .fill-container:active {
    cursor: grabbing;
  }

  .fill-container-active {
    display: none;
  }

  .fill-enter-active {
    transition: all 0.7s cubic-bezier(0.16, 1, 0.3, 1);
  }

  .fill-enter-from {
    clip-path: circle(0% at 50% 50%);
  }
</style>

Complete Example with Drop Zone Animations ​

This example shows how to coordinate animations between draggable elements and drop zones:

vue
<script setup lang="ts">
  import { ref } from 'vue';
  import Draggable from './Draggable.vue';
  import { useDnDStore, useDroppable } from '@vue-dnd-kit/core';

  const dropped = ref(false);

  const { elementRef, isOvered } = useDroppable({
    events: {
      onDrop: () => (dropped.value = true),
    },
  });

  const { isDragging } = useDnDStore();
</script>

<template>
  <div class="animation-example">
    <Draggable
      v-if="!dropped"
      @end="dropped = false"
      class="source-draggable"
    >
      Drag me to zone
    </Draggable>

    <div
      ref="elementRef"
      class="drop-zone"
      :class="{ 'is-overed': isOvered && isDragging }"
    >
      <span class="drop-zone-label">Drop here</span>

      <Transition name="circle-skeleton">
        <div
          v-if="isOvered && isDragging"
          class="circle-skeleton"
        />
      </Transition>

      <Draggable
        v-if="dropped"
        @end="dropped = false"
        class="target-draggable"
      >
        Drag me back
      </Draggable>
    </div>
  </div>
</template>

<style>
  .animation-example {
    display: flex;
    justify-content: space-between;
    flex-direction: column;
    gap: 40px;
    padding: 20px;
  }

  .drop-zone {
    height: 200px;
    border: 2px dashed #ccc;
    border-radius: 8px;
    padding: 20px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    position: relative;
    transition: all 0.3s ease;
  }

  .drop-zone-label {
    margin-bottom: 20px;
    font-weight: 500;
    color: #666;
  }

  .is-overed {
    border-color: var(--vp-c-brand);
    background-color: rgba(var(--vp-c-brand-rgb), 0.05);
    transform: scale(1.03);
  }

  .circle-skeleton {
    width: 100px;
    height: 100px;
    border-radius: 50%;
    background-color: rgba(var(--vp-c-brand-rgb), 0.15);
    border: 2px dashed var(--vp-c-brand);
    position: absolute;
  }

  .circle-skeleton-leave-active,
  .circle-skeleton-enter-active {
    transition: all 0.3s cubic-bezier(0.165, 0.84, 0.44, 1);
  }

  .circle-skeleton-enter-from,
  .circle-skeleton-leave-to {
    opacity: 0;
    transform: scale(0.5);
  }

  .source-draggable {
    align-self: flex-start;
  }

  .target-draggable {
    margin-top: 20px;
  }
</style>

Animation Features ​

Transition Options ​

When using useDragContainer, you can specify various transition options:

js
const { elementRef } = useDragContainer({
  name: 'ripple', // Transition name
  onEnter, // Enter animation handler
  onLeave, // Leave animation handler
  enterFromClass: 'custom-enter-from',
  enterActiveClass: 'custom-enter-active',
  leaveToClass: 'custom-leave-to',
  // ... other Vue transition props
});

Tracking Animation States ​

The animating object returned by useDragContainer helps coordinate animations:

js
const { animating } = useDragContainer({
  // options
});

// animating.enter.value is true during enter animation
// animating.leave.value is true during leave animation

This is useful for coordinating the visibility of original elements while their clones are being animated.

GSAP Integration ​

For complex animations, GSAP integration works well with Vue DnD Kit:

js
function onEnter(el) {
  gsap.fromTo(
    el,
    { clipPath: 'circle(100% at 50% 50%)' },
    {
      clipPath: 'circle(50% at 50% 50%)',
      duration: 0.7,
      ease: 'elastic.inOut(2.5, 1)',
    }
  );
}

function onLeave(el, done) {
  // Remember to call done() when animation completes
  gsap.to(el, {
    opacity: 0,
    scale: 0,
    onComplete: done,
  });
}

Best Practices ​

  1. Call done() in animation callbacks: Always call the done() function in your onEnter/onLeave callbacks when using custom JavaScript animations
  2. Track animation states: Use the animating object to coordinate UI elements during transitions
  3. Coordinate original and clone visibility: Hide the original element while its clone is visible using computed properties based on animation state
  4. Use fixed positioning for drag overlays: Set position: fixed in your custom container's styles for smooth dragging
  5. Apply transition styles for smooth movement: Add transitions to the transform property for smoother cursor following

Released under the MIT License.