Skip to content

Drag to Reorder

useDraggableList() — pointer-event drag-to-reorder composable. Shows a fixed-position ghost element that follows the cursor, animates neighbouring items with translateY, and auto-scrolls the container when dragging near the edges.

ts
function useDraggableList<T>(options: UseDraggableListOptions<T>): UseDraggableListReturn

Options

items

Ref<T[]>

Reactive items array.

onReorder

(newItems, from, to) => void

Called after a successful drop with the reordered array.

isDragDisabled

(item, index) => boolean

Return true to prevent an item from being dragged.

scrollContainer

HTMLElement | Ref<HTMLElement | null>

Container for auto-scroll when dragging near its edges.

Return value

dragIndex

Readonly<Ref<number>>

Index of the item being dragged, -1 when idle.

overIndex

Readonly<Ref<number>>

Index of the current drop target.

isDragging

Readonly<Ref<boolean>>

Whether a drag is in progress.

ghostStyle

Readonly<Ref<CSSProperties>>

Fixed-position styles for the ghost element.

getItemStyle

(index: number) => CSSProperties

Per-item styles: opacity:0 for the placeholder, translateY for animated neighbours.

getItemProps

(index: number) => DraggableItemProps

Props to spread on each draggable item (data-drag-index, onPointerdown, CSS classes). Dragging is entirely pointer-event based — this never sets the native HTML5 draggable attribute, so it doesn't trigger the browser's built-in drag-ghost/text-selection behavior.

CSS classes added by getItemProps

ClassWhen
vvsk-drag--draggingApplied to the placeholder (the item being dragged)
vvsk-drag--overApplied to the current drop target
vvsk-drag--disabledApplied when isDragDisabled returns true

Auto-scroll

When scrollContainer is provided, the list automatically scrolls up or down when the cursor enters a 60 px zone near the container edges. Scroll speed is proportional to the distance (max 14 px per frame).

Example

vue
<script setup lang="ts">
import { ref } from 'vue'
import { useDraggableList } from 'vue-virtual-scroller-kit'

interface Card {
  id: number
  label: string
}

const cards = ref<Card[]>(Array.from({ length: 50 }, (_, i) => ({ id: i, label: `Card ${i + 1}` })))
const listRef = ref<HTMLElement | null>(null)

const { isDragging, dragIndex, ghostStyle, getItemStyle, getItemProps } = useDraggableList({
  items: cards,
  scrollContainer: listRef,
  onReorder: (newItems) => {
    cards.value = newItems
  },
})
</script>

<template>
  <div
    ref="listRef"
    style="display: flex; flex-direction: column; gap: 6px; overflow-y: auto; height: 500px"
  >
    <div
      v-for="(card, index) in cards"
      :key="card.id"
      v-bind="getItemProps(index)"
      :style="getItemStyle(index)"
      class="card"
    >
      ⣿ {{ card.label }}
    </div>
  </div>

  <Teleport to="body">
    <div v-if="isDragging && dragIndex >= 0" class="card card--ghost" :style="ghostStyle">
      ⣿ {{ cards[dragIndex]?.label }}
    </div>
  </Teleport>
</template>

<style>
.card {
  padding: 12px 16px;
  background: #fff;
  border: 1px solid #ddd;
  border-radius: 6px;
  cursor: grab;
  user-select: none;
}
.card--ghost {
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.3);
  transform: scale(1.02);
  pointer-events: none;
}
</style>