Skip to content

Techniques

Practical patterns and techniques for building real drag-and-drop UIs with vue-dnd-kit.


1. Dual-role element (Draggable Zone)

An element can act as both a draggable and a droppable at the same time. Register makeDraggable and makeDroppable on the same ref.

The key option is placementMargins — it splits the element into edge zones and a center zone:

Pointer positionActive roleEffect
Top / bottom edgeDraggableSort among siblings (insert line)
CenterDroppableNest inside this node (glow)
Project tree
Drag via handle · hover edges to sort · hover center to nest inside a folder
📂Frontend
📂components
📄Button.vue
📄Input.vue
📄App.vue
📄main.ts
📂Backend
📄routes.ts
📄db.ts
📄package.json
📄README.md

Dual-role: key code

ts
const nodeRef = useTemplateRef<HTMLElement>('nodeRef');

// Edge zone → sort siblings
const { isDragging, isDragOver: placement } = makeDraggable(
  nodeRef,
  {
    dragHandle: '.drag-handle',
    placementMargins: { top: 12, bottom: 12 },
  },
  () => [props.index, props.siblings]
);

// Center zone → nest inside this node
const { isDragOver: isOver } = makeDroppable(
  nodeRef,
  { events: { onDrop: (e) => emit('drop', e) } },
  () => props.node.children
);

The placementMargins value defines how many pixels from the top/bottom edge are considered the "sort" zone. Everything outside that margin is the "drop" zone.

Why use it

Without this technique, leaf nodes (no children) have no droppable element in the DOM, so dropping onto them is impossible. Making each node dual-role solves this — you can drop onto any node regardless of whether it has children.


2. Selection area (box select + multi-drag)

makeSelectionArea adds a rubber-band selection box to a container. Hold Ctrl and drag to draw a rectangle — all items intersecting it become selected. Then drag any selected item to move all of them at once.

Use Ctrl + drag to rubber-band select items, or click checkboxes. Then drag any selected item to move them all.
Homepage redesign
Fix navigation bug
Write unit tests
Update documentation
Deploy to staging

Selection area: key code

ts
const listRef = useTemplateRef<HTMLElement>('listRef');

// Activate box-select on Ctrl + drag
const { isSelecting, style: selectionStyle } = makeSelectionArea(listRef, {
  modifier: {
    keys: ['ControlLeft', 'ControlRight'],
    method: 'some', // activate if ANY of the keys is held
  },
});

// Same container is also a droppable
makeDroppable(
  listRef,
  {
    events: {
      onDrop(e: IDragEvent) {
        const r = e.helpers.suggestSort('vertical');
        if (!r) return;
        items.value = r.sourceItems as Item[];
      },
    },
  },
  () => items.value
);

Render the selection rectangle using the style returned by makeSelectionArea:

vue
<div ref="listRef" class="list">
  <div v-if="isSelecting" class="selection-rect" :style="selectionStyle" />
  <TaskItem v-for="..." />
</div>
css
.selection-rect {
  position: absolute; /* list must be position: relative */
  border: 1.5px solid var(--vp-c-brand-1);
  background: rgba(62, 175, 124, 0.1);
  pointer-events: none;
  z-index: 10;
}

Items use selected from makeDraggable to reflect and control their selection state:

ts
const { isDragging, isDragOver, selected } = makeDraggable(
  itemRef,
  { dragHandle: '.handle' },
  () => [props.index, props.items]
);
vue
<!-- Checkbox stays in sync with box-select -->
<input v-model="selected" type="checkbox" />

3. v-model pattern for groups

Each group component owns its items via defineModel. Parent just passes v-model — no handlers, no shared state, no coordination.

Kanban — v-model recursive

Drag tasks between columns · drag column headers to reorder columns

To Do3
Design mockups
Write specs
Setup CI
In Progress2
Implement DnD
Review PR #42
Done1
Project setup

v-model group: key code

Data — one array, items can hold children at any depth:

ts
interface Item {
  id: string;
  title: string;
  color?: string;
  children?: Item[];   // nested group
}

Parent — just v-model per column:

vue
<div v-for="col in columns" class="column">
  <VModelGroup v-model="col.items" />
</div>

VModelGroup — droppable zone, fully self-contained:

ts
const items = defineModel<Item[]>({ required: true });

makeDroppable(el, {
  groups: ['item'],
  events: {
    onDrop(e) {
      const r = e.helpers.suggestSort('vertical');
      if (!r) return;

      items.value = r.targetItems as Item[];

      if (!r.sameList) {
        // e.draggedItems[0].items IS the reactive array of the source group —
        // mutating it in-place triggers Vue reactivity on that group directly.
        const src = e.draggedItems[0].items as Item[];
        src.splice(0, src.length, ...(r.sourceItems as Item[]));
      }
    },
  },
}, () => items.value);

VModelItem — draggable leaf, renders a nested VModelGroup when item.children exists:

vue
<!-- Circular dep resolved via defineAsyncComponent -->
const VModelGroup = defineAsyncComponent(() => import('./VModelGroup.vue'));

<div ref="el">
  {{ item.title }}
  <VModelGroup v-if="item.children" v-model="item.children" />
</div>

Same-group sort and cross-group transfer handled entirely inside the components — no parent handlers or shared state needed.

When to use

ScenarioApproach
Flat groups / kanbanv-model + in-place source mutation (this pattern)
Deeply nested treesame pattern, items with children auto-recurse

See also

Released under the MIT License.