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.
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.
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
<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ón | Qué hace |
|---|---|
id, data, type | Quién se arrastra, qué lleva y con qué se compara el accept de los destinos. |
handle | Un selector o un elemento interior: solo él inicia un arrastre. Consulta las asas. |
disabled | No empieza ningún arrastre. |
overlay | true (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, modifiers | Bloquear el arrastre en 'x' o 'y', ajustarlo a una cuadrícula, mantenerlo dentro de unos límites: consulta las restricciones. |
keyboardMode | Cómo lo mueven las flechas: por píxeles (por defecto) o de destino en destino (más abajo). |
activation, canDrag | Cuándo una pulsación se convierte en arrastre: consulta los sensores. |
attributes | role, tabindex y aria-* para el teclado y los lectores de pantalla (true por defecto). |
| Devuelve | Qué es |
|---|---|
isDragging | Este elemento se está arrastrando. |
selected | Modificable, ligado a la selección del provider. |
transform | El desplazamiento del arrastre, para overlay: false. |
id | El 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
<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>
| Devuelve | true cuando |
|---|---|
isOver | El arrastre está sobre este destino, lo acepte o no. |
isAccepting | Se está arrastrando algo y este destino lo acepta: resalta los destinos válidos. |
placement | No 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ón | Qué hace |
|---|---|
id, data | Se leen como over.id y over.data. |
accept, acceptMode | Qué arrastres acepta (siguiente sección). |
disabled | No acepta nada y los arrastres pasan por encima. |
priority | Gana el mayor cuando los destinos se solapan sin estar anidados (0 por defecto). |
onDragEnter, onDragLeave | El arrastre entró o salió. |
onBeforeDrop, onDrop | Decidir 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:
accept | Acepta |
|---|---|
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 }) => boolean | Lo 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:
{
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:
<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 evento | Qué contiene |
|---|---|
active | El arrastre: id, ids, items (cada uno { id, data, type, item }), data, type. |
over | Este destino: id, data, element, rect, accepted. |
pointer, delta | Dónde está el puntero y cuánto se ha movido desde el inicio. |
position | Dónde queda la caja arrastrada dentro de este destino (siguiente sección). |
source | Presente 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ó.
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>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';
},
});
<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.
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.
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)` }),
}));
positiones 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:
transformdescuenta 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: hiddenlo 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:
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>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].itemes el propio ítem de la lista. Guárdalo, y un deshacer lo devuelve a donde estaba.- El
onDroppropio de la lista se ejecuta entonces conto.index === -1, ye.resultcontiene 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.
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:
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>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
acceptrechaza 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:
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>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
undefinedpara rechazar. Las listas sincreateItemignoran 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:
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>const { isOver, isAccepting } = makeDroppable(el, {
external: { files: 'image/*', multiple: false },
onDrop: (e) => show(e.external!.files[0]!),
});
filesse lee como<input accept>: tipos MIME (image/*,application/pdf) y extensiones (.md).trueacepta cualquier archivo, una función decide archivo por archivo.linksytextaceptan lo que arrastran otras apps y pestañas,typescualquier 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 sinexternallo 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; conmultiple: 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.
<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:
keyboardMode | Una 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.