Documentación/Guías

Arrastrar y soltar

Arrastre libre con makeDraggable y destinos de drop con makeDroppable — destinos con tipo que aceptan unos arrastres y rechazan otros, un plano de planta, zonas de papelera, carpetas dentro de una lista e ítems sueltos que una lista acoge.

Editar esta página

Esta página la ha traducido una IA y puede contener imprecisiones. Leer el original en inglés

Usa makeDraggable y makeDroppable para todo lo que no sea una lista ordenable: residuos en contenedores, muebles en un plano, archivos en carpetas, una papelera, un tablero de ajedrez.

Live

Cargando la demo…

<script setup lang="ts">
import { reactive } from 'vue';
import { DnDProvider, type Key } from '@vue-dnd-kit/core';
import Bin from './Bin.vue';
import { BINS, WASTE } from './waste';

// The tray takes everything back (no `accept`); the bins take what their rule allows.
const TRAY = { id: 'tray', name: 'To sort', color: '#42b883', accept: '*', rule: "'*'" };

/** Where each item is: the tray or a bin. */
const place = reactive<Record<string, string>>(Object.fromEntries(WASTE.map((waste) => [waste.id, 'tray'])));
const itemsIn = (bin: string) => WASTE.filter((waste) => place[waste.id] === bin);
const drop = (bin: string, id: Key) => (place[String(id)] = bin);
</script>

<template>
  <DnDProvider>
    <div class="recycling">
      <Bin :bin="TRAY" :items="itemsIn('tray')" tray @drop="drop('tray', $event)" />
      <div class="bins">
        <Bin v-for="bin in BINS" :key="bin.id" :bin="bin" :items="itemsIn(bin.id)" @drop="drop(bin.id, $event)" />
      </div>
    </div>
  </DnDProvider>
</template>

<style scoped>
.recycling {
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-width: 600px;
  margin: 0 auto;
}

.bins {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px;
}

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

Un elemento arrastrable

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

const props = defineProps<{ waste: { id: string; name: string; type: string } }>();
const el = useTemplateRef<HTMLElement>('el');

const { isDragging } = makeDraggable(el, {
  id: () => props.waste.id,      // read back as `active.id`
  type: () => props.waste.type,  // matched by the targets' `accept`
  data: () => props.waste,       // anything: `active.data`
});
</script>

<template>
  <div ref="el" :class="{ dragging: isDragging }">{{ waste.name }}</div>
</template>

Pasa el id de tus datos como getter. El id por defecto viene de useId() y es nuevo en cada instancia del componente, así que no significa nada para el resto de tu app. Las opciones se leen cuando hacen falta, así que un getter o una ref las mantiene al día.

OpciónQué hace
id, data, typeQuién se arrastra, qué lleva y con qué se compara el accept de los destinos.
handleUn selector o un elemento interior: solo él inicia un arrastre. Consulta las asas.
disabledNo empieza ningún arrastre.
overlaytrue (por defecto): una copia sigue al puntero. Un componente: tu propia vista previa. false: el elemento se mueve por sí mismo (más abajo).
axis, modifiersBloquear el arrastre en 'x' o 'y', ajustarlo a una cuadrícula, mantenerlo dentro de unos límites: consulta las restricciones.
keyboardModeCómo lo mueven las flechas: por píxeles (por defecto) o de destino en destino (más abajo).
activation, canDragCuándo una pulsación se convierte en arrastre: consulta los sensores.
attributesrole, tabindex y aria-* para el teclado y los lectores de pantalla (true por defecto).
DevuelveQué es
isDraggingEste elemento se está arrastrando.
selectedModificable, ligado a la selección del provider.
transformEl desplazamiento del arrastre, para overlay: false.
idEl id en uso.

Los botones, los inputs y los demás hijos interactivos no inician un arrastre de puntero, así que siguen funcionando dentro de un elemento arrastrable. Todas las opciones están en la API de makeDraggable.

Un destino de drop

Bin.vue
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDroppable, type Key } from '@vue-dnd-kit/core';
import type { Bin, Waste } from './waste';

const props = defineProps<{ bin: Bin; items: Waste[] }>();
const emit = defineEmits<{ drop: [id: Key] }>();
const el = useTemplateRef<HTMLElement>('el');

const { isOver, isAccepting } = makeDroppable(el, {
  id: props.bin.id,
  accept: props.bin.accept,   // 'glass', ['paper', 'cardboard'], a function…
  onDrop: (event) => emit('drop', event.active.id),
});
</script>

<template>
  <section ref="el" :class="{ accepting: isAccepting, over: isOver && isAccepting, refused: isOver && !isAccepting }">
    <WasteItem v-for="waste in items" :key="waste.id" :waste="waste" />
  </section>
</template>
Devuelvetrue cuando
isOverEl arrastre está sobre este destino, lo acepte o no.
isAcceptingSe está arrastrando algo y este destino lo acepta: resalta los destinos válidos.
placementNo es un booleano: { top, right, bottom, left }, las mitades del destino en las que está el puntero, o null cuando no está encima. Úsalo para «antes / después de esta tarjeta».

isOver sin isAccepting significa que el arrastre está sobre un destino que lo rechaza. Muéstralo, como hacen los contenedores de arriba: un drop ahí se cancela y el ítem vuelve a su sitio. Los contenedores que no pueden aceptar el ítem se atenúan mientras viaja; más ideas en estilos.

OpciónQué hace
id, dataSe leen como over.id y over.data.
accept, acceptModeQué arrastres acepta (siguiente sección).
disabledNo acepta nada y los arrastres pasan por encima.
priorityGana el mayor cuando los destinos se solapan sin estar anidados (0 por defecto).
onDragEnter, onDragLeaveEl arrastre entró o salió.
onBeforeDrop, onDropDecidir el drop y luego aplicarlo (más abajo).

Qué arrastres acepta un destino

El type de un elemento arrastrable se compara con el accept de un destino:

acceptAcepta
omitido o '*'Todo.
'glass'Los elementos arrastrables de tipo glass y los que no tienen tipo.
['paper', 'cardboard']Cualquiera de los dos tipos y los que no tienen tipo.
({ active, items, types }) => booleanLo que diga tu función.

Un elemento arrastrable sin type cumple cualquier regla de tipo string. type también puede ser un array (['paper', 'large']): basta con que coincida un tipo. Una función decide según cualquier aspecto del arrastre. El contenedor de compost de arriba lee los datos de los ítems, no su tipo:

waste.ts
{
  id: 'compost',
  accept: ({ items }) => items.every((item) => (item.data as Waste).compostable),
}

accept se lee en vivo, mientras el arrastre busca su destino: pasa una ref para cambiarlo.

Cuando se arrastran varios ítems a la vez, acceptMode indica cuántos deben encajar: 'all' (por defecto) o 'some'. Con 'some', devuelve desde onBeforeDrop los ids que quieres conservar, y los demás vuelven a su sitio. Consulta decidir un drop.

El drop

El onDrop de un destino de drop simple es donde tú cambias tus datos. La biblioteca no mueve nada por su cuenta:

index.vue
<script setup lang="ts">
/** Where each item is: the tray or a bin. */
const place = reactive<Record<string, string>>(Object.fromEntries(WASTE.map((waste) => [waste.id, 'tray'])));
const itemsIn = (bin: string) => WASTE.filter((waste) => place[waste.id] === bin);
const drop = (bin: string, id: Key) => (place[String(id)] = bin);
</script>

<template>
  <Bin v-for="bin in BINS" :key="bin.id" :bin="bin" :items="itemsIn(bin.id)" @drop="drop(bin.id, $event)" />
</template>
Campo del eventoQué contiene
activeEl arrastre: id, ids, items (cada uno { id, data, type, item }), data, type.
overEste destino: id, data, element, rect, accepted.
pointer, deltaDónde está el puntero y cuánto se ha movido desde el inicio.
positionDónde queda la caja arrastrada dentro de este destino (siguiente sección).
sourcePresente cuando los ítems vienen de una lista ordenable: { list, remove() }.

active.ids contiene todos los ids arrastrados, empezando por el que se agarró. El orden es onBeforeDrop (decidir), onDrop (aplicar) y luego el drag-end del provider. Para escuchar todos los arrastres de la página en un solo sitio, usa useDnDMonitor (consulta el provider). Para preguntar antes de un drop, mantener el arrastre o aceptar solo algunos ítems, consulta decidir un drop.

Dejarlo donde se soltó

Un plano de planta, un lienzo, un tablero: el ítem se queda donde lo sueltas. Cada evento lleva position, el left / top que coloca un ítem con posición absoluta donde se soltó.

Live

Cargando la demo…

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

<template>
  <DnDProvider>
    <Planner />
  </DnDProvider>
</template>
Planner.vue
const room = useTemplateRef<HTMLElement>('room');
makeDroppable(room, {
  id: 'room',
  onDrop: ({ active, position }) => {
    const piece = find(active.id);
    if (!piece || !position || !room.value) return;
    // Clamped, so a piece let go half outside stays inside the walls.
    piece.x = clamp(position.x, room.value.clientWidth - piece.width);
    piece.y = clamp(position.y, room.value.clientHeight - piece.height);
    piece.place = 'room';
  },
});
Piece.vue
<div ref="el" class="piece" :style="{ left: `${piece.x}px`, top: `${piece.y}px` }">…</div>

position es la esquina superior izquierda de la caja arrastrada dentro de over, medida desde el borde de su padding, con su scroll sumado: el borde de la habitación, sus paredes, no cuenta. Es null cuando no hay nada debajo.

No calcules el sitio a partir de active.rect y delta: así se pierden el borde y el scroll del destino. Las cuadrículas, los límites y el bloqueo de eje para este tipo de tableros están en restricciones.

Sin overlay

Por defecto, una copia del elemento sigue al puntero y el elemento se queda en su sitio: así funcionan los muebles del almacén. Con overlay: false no hay copia, y el elemento se mueve por sí mismo con el transform que devuelve el arrastre: los muebles de la habitación.

Piece.vue
const inRoom = computed(() => props.piece.place === 'room');
const { isDragging, transform } = makeDraggable(el, {
  id: () => props.piece.id,
  overlay: computed(() => !inRoom.value),   // a value or a ref, read when the drag starts
});

const style = computed(() => ({
  left: `${props.piece.x}px`,
  top: `${props.piece.y}px`,
  ...(transform.value && { transform: `translate3d(${transform.value.x}px, ${transform.value.y}px, 0)` }),
}));
  • position es entonces la esquina del propio elemento, así que el manejador del drop no cambia.
  • El elemento está bajo el puntero mientras se mueve; al buscar el destino, la biblioteca mira a través de él. Ponlo por encima de sus vecinos mientras se arrastra (z-index).
  • Se desplaza con la página, el overlay no: transform descuenta lo que su contenedor se ha movido desde que empezó el arrastre, así que sigue bajo el puntero cuando la página se desplaza.
  • Se mueve dentro de su propio contenedor: un padre con overflow: hidden lo recorta, y su contexto de apilamiento decide por encima de qué pasa. El overlay no tiene ninguno de los dos problemas.

Para una vista previa propia en lugar de la copia, consulta el overlay.

Zonas de papelera

Un arrastre que empieza en una lista ordenable lleva e.source. Llama a remove() y la lista quita los ítems:

Live

Cargando la demo…

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

<template>
  <DnDProvider>
    <Triage />
  </DnDProvider>
</template>
Triage.vue
const triage = (bin: 'done' | 'deleted') => (e: DnDEvent) => {
  bins[bin].push(...e.active.items.map((entry) => entry.item as Notice));
  e.source?.remove();
};

makeDroppable(doneZone, { id: 'done', onDrop: triage('done') });
makeDroppable(trashZone, { id: 'trash', onDrop: triage('deleted') });
  • e.active.items[i].item es el propio ítem de la lista. Guárdalo, y un deshacer lo devuelve a donde estaba.
  • El onDrop propio de la lista se ejecuta entonces con to.index === -1, y e.result contiene la lista sin los ítems.
  • El overlay se encoge dentro de la zona, ya que el ítem no se muestra en ningún sitio.
  • Una lista también puede quitar lo que se suelta fuera de todo: consulta dropOutside.
Con el teclado, las flechas mueven un ítem de una lista entre los huecos de su lista y las listas de su grupo; más allá del final de las listas, la siguiente pulsación lo lleva al destino de drop en esa dirección, aquí la papelera, y la flecha contraria lo devuelve. Un botón, un menú o la tecla Delete siguen siendo más rápidos para mucha gente: ofrece uno también.

Soltar dentro de un ítem ordenable

Una fila de una lista ordenable también puede ser un destino de drop: una carpeta que acepta marcadores, un grupo que acepta tareas. Pon un droppable en el elemento del ítem:

Live

Cargando la demo…

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

<template>
  <DnDProvider>
    <Bookmarks />
  </DnDProvider>
</template>
FolderRow.vue
const el = useTemplateRef<HTMLElement>('el');
makeSortableItem(el, { item: () => props.folder, type: 'folder' });

const { isOver } = makeDroppable(el, {
  accept: 'link',
  onDrop: (e) => {
    emit('add', e.active.items.map((entry) => entry.item as Link)); // Bookmarks adds them
    e.source?.remove();   // take them out of the list they came from
  },
});

Una carpeta abierta muestra sus enlaces como una lista del mismo grupo, que solo acepta enlaces: ahí se ordenan, y se pueden arrastrar de vuelta afuera, al nivel superior o a otra carpeta.

Donde la lista ordenaría el arrastre, un destino así solo cuenta en el centro de su ítem (del 25 al 75 % a lo largo del eje de la lista). Ahí el drop va dentro del ítem y la lista no se mueve. Cerca de los bordes, la lista ordena como siempre.

  • Un destino cuyo accept rechaza el arrastre se ignora: la lista ordena sobre el ítem como sobre cualquier otro. Las carpetas solo aceptan enlaces, así que una carpeta arrastrada se ordena pasando de largo por las demás.
  • Un destino nunca recibe su propio arrastre.
  • Con el teclado, una flecha se detiene primero en la carpeta; la siguiente pulsación en la misma dirección pasa de largo.
  • El ítem deja de mostrarse tras el drop, así que el overlay se encoge dentro de la carpeta en lugar de volver a su sitio.
  • Sobre la carpeta, la lista mantiene el hueco de la fila arrastrada donde estaba, así que nada se mueve bajo el puntero. El hueco recibe entonces data-dnd-placeholder="into": el drop no va ahí, así que ocúltalo si quieres ([data-dnd-placeholder='into'] { opacity: 0 }).

El gestor de archivos hace lo mismo en una cuadrícula, con carpetas anidadas.

Elementos arrastrables simples en una lista

Una lista ordenable acoge ítems arrastrados con makeDraggable cuando sabe crear un ítem a partir de ellos:

Live

Cargando la demo…

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

const BLOCKS = [
  { title: 'Focus', minutes: 90, color: '#42b883' },
  { title: 'Meeting', minutes: 30, color: '#3b82f6' },
  { title: 'Lunch', minutes: 60, color: '#f59e0b' },
  { title: 'Gym', minutes: 45, color: '#ec4899' },
  { title: 'Break', minutes: 15, color: '#a1a1aa' },
];
</script>

<template>
  <DnDProvider>
    <div class="planner">
      <aside class="blocks">
        <h4>Blocks</h4>
        <BlockChip v-for="block in BLOCKS" :key="block.title" :block="block" />
      </aside>
      <Agenda />
    </div>
  </DnDProvider>
</template>

<style scoped>
.planner {
  display: flex;
  justify-content: center;
  gap: 24px;
}

.blocks {
  display: flex;
  flex: none;
  flex-direction: column;
  gap: 6px;
  width: 140px;
}

h4 {
  margin: 0 0 6px;
  font: 600 11px/1 ui-monospace, Menlo, Consolas, monospace;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.55;
}

@media (max-width: 480px) {
  .blocks {
    width: 112px;
  }
}
</style>
Agenda.vue
const { items } = makeSortableList(list, {
  items: agenda,
  createItem: (active) => ({ ...(active.data as Block), id: `entry-${++created}` }),
});

Mientras el elemento arrastrable está sobre la lista, el ítem creado está en el borrador y tu plantilla lo renderiza como cualquier otra fila: las horas de la agenda ya lo cuentan. Se vuelve real al soltar (mode: 'insert', from.list === null), y el overlay aterriza sobre él.

  • Devuelve undefined para rechazar. Las listas sin createItem ignoran los elementos arrastrables simples.
  • Dale al nuevo ítem una clave propia: el bloque se queda en la paleta.
  • Con el teclado, una flecha hacia la lista mete el elemento arrastrable en ella, a su altura, y las siguientes flechas lo mueven de hueco en hueco.

Archivos desde el equipo

Los archivos arrastrados desde el escritorio o un gestor de archivos no son arrastres de la biblioteca: los lleva el navegador. Un destino de drop los acepta con external, y el drop los trae en e.external:

Live

Cargando la demo…

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

<template>
  <DnDProvider>
    <div class="form">
      <AvatarDrop />
      <Attachments />
    </div>
  </DnDProvider>
</template>

<style scoped>
.form {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  justify-content: center;
  gap: 16px;
}
</style>
AvatarDrop.vue
const { isOver, isAccepting } = makeDroppable(el, {
  external: { files: 'image/*', multiple: false },
  onDrop: (e) => show(e.external!.files[0]!),
});
  • files se lee como <input accept>: tipos MIME (image/*, application/pdf) y extensiones (.md). true acepta cualquier archivo, una función decide archivo por archivo. links y text aceptan lo que arrastran otras apps y pestañas, types cualquier otro tipo MIME.
  • El arrastre funciona como un arrastre del provider: isOver, isAccepting, los eventos y los anuncios funcionan como con cualquier otro. Los destinos sin external lo ignoran. No hay overlay: el navegador dibuja la imagen arrastrada.
  • Mientras los archivos se mantienen sobre la página, el navegador solo dice sus tipos (e.external.fileTypes). Sus nombres, tamaños y contenidos llegan con el drop, y es entonces cuando se comprueban las extensiones. Las carpetas se descartan; con multiple: false, un arrastre de varios archivos se rechaza.

Las listas también aceptan archivos, enlaces y texto, y les hacen sitio mientras se mantienen. Los ítems también pueden salir de la página, a otra app o a otra ventana tuya: consulta archivos y otras apps.

Quien usa el teclado o un lector de pantalla no puede arrastrar archivos. Ofrece un <input type="file"> junto a la zona, como hace la demo con Browse.

Teclado

Un elemento arrastrable con el foco se agarra con Space o Enter, se suelta con las mismas teclas y Escape cancela. Entre medias, las flechas lo mueven:

keyboardModeUna flecha
'pixels' (por defecto)Lo mueve 25 px, el keyboard.step del provider. Con Shift, cuatro veces más lejos. Así se mueven los muebles.
'droppables'Salta al destino más cercano en esa dirección que lo acepte. Los residuos saltan de contenedor en contenedor y se saltan los que los rechazan.
makeDraggable(el, { id: () => props.waste.id, type: () => props.waste.type, keyboardMode: 'droppables' });

Las teclas se pueden reasignar en el provider. Más en accesibilidad.