Docs/API

DnDProvider

The component that owns the drag — input, the overlay, the selection, auto-scroll and the live region — for everything rendered inside it.

const DnDProvider: Component<DnDProviderProps>;

Owns the drags of everything rendered inside it: input, the overlay, the selection, auto-scroll and the screen reader announcements (see DnDProviderProps).

App.vue
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
</script>

<template>
  <DnDProvider :animation="{ duration: 200 }" @drag-end="save">
    <RouterView />
    <template #overlay="{ items, over, accepted }">
      <!-- optional: a custom overlay for every drag -->
    </template>
  </DnDProvider>
</template>

Every composable must run in a component rendered inside a provider. One provider for the whole app is the goal: parts of the page get their own rules with makeDragArea. See the provider guide.

Most props are read when the provider needs them: change one, and the next drag uses it.

Live

Loading the demo…

<script setup lang="ts">
import { computed, ref } from 'vue';
import { DnDProvider, type ActivationOptions } from '@vue-dnd-kit/core';
import PropRow from './PropRow.vue';
import DensityList from './DensityList.vue';

const ACTIVATIONS: Record<string, ActivationOptions | undefined> = {
  default: undefined,
  '{ mouse: { delay: 300 } }': { mouse: { delay: 300, tolerance: 5 } },
};

const overlayFit = ref<'slot' | 'source'>('slot');
const activation = ref('default');
const keyboard = ref<'true' | 'false'>('true');
const copyModifier = ref<'Alt' | 'Shift' | 'null'>('Alt');

// The provider reads its props when it needs them: a change applies to the next drag.
const props = computed(() => ({
  overlayFit: overlayFit.value,
  activation: ACTIVATIONS[activation.value],
  // `false` turns keyboard drags off; an object sets their keys and steps.
  keyboard: keyboard.value === 'true' ? undefined : (false as const),
  copyModifier: copyModifier.value === 'null' ? null : copyModifier.value,
}));

const compact = ref([
  { id: 'copy', title: 'Write copy' },
  { id: 'login', title: 'Fix login' },
]);
const roomy = ref([
  { id: 'sprint', title: 'Plan the sprint' },
  { id: 'review', title: 'Review the PR' },
]);
</script>

<template>
  <DnDProvider v-bind="props">
    <div class="demo">
      <div class="props" role="group" aria-label="DnDProvider props">
        <PropRow v-model="overlayFit" name="overlay-fit" :options="['slot', 'source']" />
        <PropRow v-model="activation" name=":activation" :options="Object.keys(ACTIVATIONS)" />
        <PropRow v-model="keyboard" name=":keyboard" :options="['true', 'false']" />
        <PropRow v-model="copyModifier" name="copy-modifier" :options="['Alt', 'Shift', 'null']" />
      </div>
      <div class="lists">
        <DensityList v-model="compact" title="Compact" compact />
        <DensityList v-model="roomy" title="Roomy" />
      </div>
    </div>
  </DnDProvider>
</template>

<style scoped>
/* Room for every row in one list: the lists grow downward and nothing moves under the pointer. */
.demo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  min-height: 430px;
}

.props {
  display: grid;
  grid-template-columns: auto auto;
  align-items: center;
  gap: 6px 14px;
  max-width: 100%;
  padding: 12px 14px;
  border: 1px solid color-mix(in srgb, currentColor 12%, transparent);
  border-radius: 12px;
  background: color-mix(in srgb, currentColor 3%, Canvas);
}

.lists {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: center;
  gap: 14px;
}

@media (max-width: 480px) {
  .props {
    grid-template-columns: minmax(0, 1fr);
  }
}
</style>

Where the overlay renders

overlayTo moves the overlay out of body: into a modal <dialog>, which sits above everything else on the page. Turn the switch off, and the rows you drag vanish under the dialog. The overlay guide explains when it is needed.

Live

Loading the demo…

<script setup lang="ts">
import { computed, ref, useTemplateRef } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import ColumnList from './ColumnList.vue';

const columns = ref(['Name', 'Status', 'Owner', 'Due date', 'Priority']);

const dialog = useTemplateRef<HTMLDialogElement>('dialog');
const open = ref(false);
const intoDialog = ref(true);

// A modal <dialog> sits on the top layer, above everything in <body>, the overlay
// included. While it is open, the overlay renders inside it.
const overlayTo = computed(() => (open.value && intoDialog.value ? '#columns-dialog' : 'body'));

const show = () => {
  dialog.value?.showModal();
  open.value = true;
};
</script>

<template>
  <DnDProvider :overlay-to="overlayTo">
    <div class="sheet">
      <table>
        <thead>
          <tr>
            <th v-for="column in columns" :key="column">{{ column }}</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="row in 2" :key="row">
            <td v-for="column in columns" :key="column"><span class="skeleton" /></td>
          </tr>
        </tbody>
      </table>
      <button type="button" class="primary" @click="show">Edit columns</button>
    </div>

    <dialog id="columns-dialog" ref="dialog" @close="open = false">
      <h3>Columns</h3>
      <p>Drag to change their order.</p>
      <ColumnList v-model:columns="columns" />
      <footer>
        <label class="switch">
          <input v-model="intoDialog" type="checkbox" />
          Overlay inside the dialog
        </label>
        <button type="button" class="primary" @click="dialog?.close()">Done</button>
      </footer>
    </dialog>
  </DnDProvider>
</template>

<style scoped>
.sheet {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  width: 100%;
}

table {
  width: min(100%, 520px);
  border-collapse: collapse;
  font-size: 12px;
}

th {
  padding: 8px;
  border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent);
  font-weight: 600;
  text-align: left;
  white-space: nowrap;
}

td {
  padding: 10px 8px;
  border-bottom: 1px solid color-mix(in srgb, currentColor 8%, transparent);
}

.skeleton {
  display: block;
  height: 8px;
  border-radius: 4px;
  background: color-mix(in srgb, currentColor 10%, transparent);
}

button {
  padding: 7px 14px;
  border: 0;
  border-radius: 8px;
  font: 600 12px/1.2 system-ui, sans-serif;
  cursor: pointer;
}

.primary {
  background: #42b883;
  color: #04130c;
}

button:focus-visible {
  outline: 2px solid #42b883;
  outline-offset: 2px;
}

/* No transform on the dialog: it would move the overlay's fixed position. */
dialog {
  width: min(340px, calc(100vw - 32px));
  box-sizing: border-box;
  margin: auto;
  padding: 18px;
  border: 1px solid color-mix(in srgb, currentColor 14%, transparent);
  border-radius: 16px;
  background: Canvas;
  color: CanvasText;
  box-shadow: 0 30px 60px -20px rgb(0 0 0 / 0.6);
}

dialog::backdrop {
  background: rgb(0 0 0 / 0.45);
}

h3 {
  margin: 0;
  font-size: 15px;
}

p {
  margin: 4px 0 14px;
  font-size: 12px;
  opacity: 0.6;
}

footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 16px;
}

.switch {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
}

.switch input {
  accent-color: #42b883;
}
</style>

Props

Props of DnDProvider. Most are read when the provider needs them: change one, and the next drag uses it.

CollisionDetection

How the target under the drag is chosen. Default pointerFirst; another built-in (closestCorners) or your own.

Modifier[]

Modifiers for every drag, applied first: those of drag areas, draggables and lists come after.

LayoutAnimation

Default FLIP animation for sortable lists.

DropAnimation

How the overlay lands on drop and flies back on cancel, when nothing closer sets one: the drop target's overDropAnimation comes first, then the dragged item's own (or its list's), then this one.

AutoAnimation

Default animateChanges of sortable lists and trees: changes of their items that no drag made. Off by default.

boolean | AutoScrollOptions

Scrolls containers and the page when a drag nears their edges. Default true; options tune it, false turns it off. Lists, trees and drag areas can override it.

ActivationOptions

What a press must do before it becomes a drag, per pointer kind (see ActivationOptions for the defaults). Drag areas, lists, trees, items and draggables can override it.

KeyboardOptions | false

Keyboard drags: their keys and step. false turns them off; Escape still cancels any drag.

Sensor[]

What turns input into drags. Default [pointerSensor, keyboardSensor]: leave one out, wrap or replace it, or add your own (defineSensor). The keyboard sensor also cancels any drag with Escape; keyboard: false turns its drags off but keeps that. The list may change at any time: sensors that leave it stop, cancelling their drag.

boolean | HapticsOptions

Vibration for touch drags. Default: a short one when a drag starts after the long press; an object adds others (enter, drop, refuse); false turns them all off.

string | null

Key (KeyboardEvent.key) that turns a move into a copy. Default 'Alt'; null disables.

string | HTMLElement

Where the overlay renders: a selector or an element. Default 'body'. A modal <dialog> sits above the rest of the page: drags inside one need it there.

'slot' | 'source'

Overlay size while dragging. 'slot' (default): it follows the slot the item will land in, so over another list it takes the size of that list's items, and the drop needs no resize. It resizes around the pointer, which stays at the same fraction of the box. 'source': it keeps the dragged element's size. The default overlay content re-lays out at the new size; it is never scaled.

number | 'all'

Multi-drag: how many dragged items get a clone at pickup, the grabbed one and the others next to it in drop order. Default 5; 'all' clones every one that is rendered. The default overlay shows the clones and counts the rest (+N); in a layout of your own, DragPreview renders its slot for an item without one. Every clone flies: back to its item on a cancel, to its slot on a drop, into the zone a drop goes into, one after another. The clones are taken together, one layout per snapshot pass for all, but each costs reading its styles: mind long items on slow phones.

boolean | TransitionProps

How the overlay changes from one component to another during a drag (a drop target's overOverlay takes over, an overlay ref is switched): true (default) crossfades them in 150 ms, false swaps them at once, and an object is the props of a Vue <Transition> around the content's root (name, mode, JS hooks for GSAP or motion). Without mode: 'out-in' both show at once: give the leaving one position: absolute (.<name>-leave-active) so they stack in the box.

string | Record<string, boolean> | Array<string | Record<string, boolean>>

Classes for the overlay's content ([data-dnd-overlay-content]), the element that fills the overlay box: the one to animate as a whole while the library moves and sizes the box. A transition on it runs from @starting-style as the drag picks up, and back as it lands ([data-dnd-overlay='dropping']): the drop waits for it. Plain CSS reaches it outside <style scoped> (or with :global(...)), Tailwind with starting: and in-data-[dnd-overlay=dragging]:.

Partial<Announcements> | false

Own texts for some events (see Announcements); false keeps the live region silent.

string

How to drag with the keyboard, read out by screen readers for every draggable (its aria-describedby). Default: English instructions for Space, Enter, the arrows and Escape.

boolean

Inject the default looks: zero-specificity, layered CSS that dims placeholders and the items a multi-drag leaves in place (opacity: 0.4). Default true; false leaves the looks to you. What drags need is injected either way: no text selection and no scroll anchoring while dragging (virtual lists rely on it), the drag cursor (--dnd-cursor), and no link callout on a long press on iOS.

Events

EventPayloadWhen
drag-startDnDEventA drag started (after its activation).
drag-moveDnDEventThe pointer moved during a drag (once per frame at most).
drag-overDnDEventThe target under the drag changed.
drag-endDnDEndEventA drop went through and was written.
drag-cancelDnDEventThe drag was cancelled or the drop refused.
drag-resumeDnDEventonBeforeDrop answered 'continue': there was no drop, and the drag goes on.
drag-settleDnDEndEvent or DnDEventThe drag's overlay has landed: what drag-end or drag-cancel got.

The same events, plus onBeforeDrop, are available anywhere under the provider with useDnDMonitor. Their payloads are on the events page.

Slot #overlay

Renders what follows the pointer, for every drag that has no overlay component of its own (see the overlay guide). Rendering nothing falls back to the clone.

What the overlay shows: the props of the provider's #overlay slot and of overlay components (overlay option of draggables, sortable items, lists and trees).

Active

The drag.

DragItem[]

Every dragged item, the primary one first: one overlay for the whole multi-drag.

Over | null

The drop target under the drag. While a decided drop animates, the target it lands on (as it was at the drop); null while a cancelled drag flies back.

boolean

Whether over accepts the dragged items; false when over nothing.

DnDStatus

'dragging', then 'dropping' while the overlay lands (or flies back).

DragMode

What a drop would do now (see DnDEvent.mode): e.g. show a plus while it copies.

DropDestination | null

Where a drop would put the dragged items now, in a sortable list or tree: e.g. tint the overlay by the folder a tree drag would go into. Outside every target it is the slot's place, where dropOutside: 'placeholder' (the default) keeps them. null when the drop would put them in no list: over a plain drop target, over a list that refuses them, outside with dropOutside: 'cancel' or 'remove'. While a decided drop animates, the place it lands on.

Keyboard

Keyboard drags (the provider's keyboard): their keys, and the step of free drags.

{ start?: string[]; drop?: string[]; cancel?: string[] }

The keys that pick up (start), drop and cancel, as KeyboardEvent.code ('Space', 'KeyW') or .key ('Enter', 'w'). Default: Space and Enter pick up and drop, Escape cancels. The arrows always move.

number

Pixels per arrow press for free (non-sortable) drags. Default 25.

Auto-scroll

How containers scroll when a drag nears their edges (the provider's autoScroll).

number

Edge zone as a fraction of the container size. Default 0.2 (clamped to 24–96px).

number

Maximum speed in px per 16ms frame.

number

Speed in px per frame with the pointer at the container's edge: slower inside the edge zone, faster past the edge, up to maxSpeed. Default 10.

Haptics

When touch drags vibrate. Only drags started by touch do; where the browser can't vibrate (iOS Safari) or blocks it (no tap on the page yet), nothing happens.

HapticPattern

The drag starts, after the long press. Default true.

HapticPattern

The drag enters a drop target (a list, a zone) that accepts it. Default false.

HapticPattern

The drop goes through. Default false.

HapticPattern

The drop is refused (by a target or onBeforeDrop) and the items fly back. Default false.

A vibration (navigator.vibrate): its length in ms or an on/off pattern. true is the default one (10ms; [20, 40, 20] for refuse), false or 0 none.

type HapticPattern = boolean | number | number[]

Announcements

Texts for screen readers, per event. Return undefined for the built-in text, '' for none. context has the positions the built-in texts of sortable lists speak.

A text is spoken by the provider's live region (moves once the drag rests for 500 ms); undefined leaves the event to the next source (the texts of integrations such as sortable positions, then the defaults). A move isn't announced for the target found at pickup: nothing moved yet.

(event: DnDEvent, context: AnnouncementContext) => string | undefined

A drag started.

(event: DnDEvent, context: AnnouncementContext) => string | undefined

The drag moved over another target, or to another place in a list (once it rests for 500 ms).

(event: DnDEndEvent, context: AnnouncementContext) => string | undefined

The drop went through, or the drag ended outside every target.

(event: DnDEvent, context: AnnouncementContext) => string | undefined

The drag was cancelled, or its drop refused.

(event: DnDEvent, context: AnnouncementContext) => string | undefined

onBeforeDrop answered 'continue': the drag goes on.

What an announcement is about, beyond the event: for texts of your own (another language, other words) that say what the built-in ones say.

AnnouncedPosition

In a sortable list or tree: where the dragged items are. At the start, where they were picked up; while moving, where they would land (none outside the lists); at the end, where they landed, or where they went back to.

{ key: Key; label?: string; position: AnnouncedPosition }

A swap: the item exchanged with the dragged one, and where it is now.

number

How many items the drag carries.

boolean

The drag copies (group.pull: 'clone', the copy modifier).

A place in a sortable list or tree (its visible rows): index counts from 0, out of length.

Key

Id of the list or tree.

string

The list's label, what people call it.

number

The place, from 0: the built-in texts say index + 1.

number

How many places the list has.