Skip to content

Swap

Exchange items between two lists — useful for team assignments, role rotation, or any scenario where you want to trade positions rather than reorder.

Demo

Sprint PlanningHover over a member to swap positions
Team Alpha
Frontend & Infrastructure3 members
AC
Alex ChenFrontend
SR
Sam RiveraBackend
JK
Jordan KimDevOps
Team Beta
Design & Quality3 members
ML
Morgan LeeDesign
CP
Casey ParkQA
RW
Riley WangPM

Overview

Swap exchanges the dragged item with the one you hover over. Unlike sort, both items trade places — nothing shifts in between.

Use cases:

  • Team roster management — move a developer from one sprint team to another, pushing someone back
  • Seat / role assignment — trading seats in a classroom or conference
  • A/B comparison boards — balance two groups by swapping members

Core logic

The entire swap is driven by event.helpers.suggestSwap():

ts
function handleDrop(e: IDragEvent) {
  // Swap only makes sense when hovering over another item
  if (!e.hoveredDraggable) return;

  const r = e.helpers.suggestSwap();
  if (!r) return;

  const srcItems = e.draggedItems[0]?.items as Player[];

  // Always update the source list
  if (srcItems === teamAlpha.value) teamAlpha.value = r.sourceItems as Player[];
  else if (srcItems === teamBeta.value) teamBeta.value = r.sourceItems as Player[];

  // Cross-list swap: update the target list too
  if (!r.sameList) {
    const tgtItems = e.hoveredDraggable.items as Player[];
    if (tgtItems === teamAlpha.value) teamAlpha.value = r.targetItems as Player[];
    else if (tgtItems === teamBeta.value) teamBeta.value = r.targetItems as Player[];
  }
}

Key points:

  • Guard with hoveredDraggable — swap has no meaning when dropping on an empty zone
  • r.sameListtrue when both items are in the same array (swap within a list)
  • r.sourceItems / r.targetItems — the new arrays after the swap; apply both when cross-list

Droppable setup

Each team list registers as a droppable, exposing its items via payload:

ts
makeDroppable(listRef, {
  events: { onDrop: handleDrop },
}, () => teamAlpha.value);

Draggable items

Items expose their position via payload. isDragOver drives the visual "swap target" highlight:

vue
<script setup lang="ts">
  const { isDragging, isDragOver } = makeDraggable(itemRef, {}, () => [
    props.index,
    props.items,
  ]);

  // isDragOver is defined when another item is hovering over this one
  const isHovered = computed(() => isDragOver.value !== undefined);
</script>

<template>
  <div
    :class="{
      'is-dragging': isDragging,   // hide original while dragging
      'is-hovered': isHovered,     // highlight as swap target
    }"
  >
    <span v-if="isHovered">⇄</span>
    <slot />
  </div>
</template>

Animations

css
.swap-move {
  transition: all 0.35s cubic-bezier(0.165, 0.84, 0.44, 1);
}

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

.swap-enter-from,
.swap-leave-to {
  opacity: 0;
  transform: scale(0.9);
}

.swap-leave-active {
  position: absolute;
  width: 100%;
  pointer-events: none;
}

Swap result shape

ts
interface ISuggestSwapResult {
  sourceItems: unknown[];   // source list after swap
  targetItems: unknown[];   // target list after swap (same ref as source when sameList)
  sourceIndexes: number[];  // indexes of dragged items in source
  targetIndex: number;      // index of the hovered item (the other side of the swap)
  sameList: boolean;
}

See also

Source

Released under the MIT License.