Documentación/Guías
El overlay
Lo que sigue al puntero mientras arrastras — el clon por defecto y cómo darle estilo, componentes de overlay que cambian mientras arrastras, varios ítems a la vez, su tamaño y dónde se renderiza.
Esta página la ha traducido una IA y puede contener imprecisiones. Leer el original en inglés
Mientras arrastras, una caja sigue al puntero: el overlay. El propio elemento arrastrado se queda donde estaba, como el hueco en el que aterrizará. Al soltar, el overlay vuela hasta ese hueco y desaparece.
El overlay se teletransporta a body, fuera de tus componentes. Por defecto muestra un clon del
elemento arrastrado.
Cargando la demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Expenses from './Expenses.vue';
</script>
<template>
<DnDProvider>
<Expenses />
</DnDProvider>
</template>El clon por defecto
El clon se toma al agarrar, antes de que nada se mueva. Es una copia profunda del elemento:
- Se copian los valores de los formularios y los píxeles de los
<canvas>. - Los estilos calculados que el elemento perdería fuera de su lista se ponen en línea: fuentes y
colores heredados, estilos que se aplican a través de los ancestros (
.list li { … }), anchos de las celdas de tabla. Una fila de tabla llega envuelta en una tabla propia. - Se quitan los
idy los atributosdata-dnd-*, y el clon esaria-hidden. - Los ripples y otros efectos de pulsación de los kits de UI (Vuetify, Quasar, PrimeVue, Naive UI) se dejan fuera.
En la tabla de arriba, una fila en la que escribiste viaja con su texto, su importe, su marca y su gráfico, en una tabla propia cuyas celdas conservan los anchos de las reales.
Marca cualquier otra cosa que no deba viajar con data-dnd-no-clone: un botón de menú, un
tooltip, una marca de selección. El botón de quitar de cada fila se queda atrás:
<tr ref="el" class="row">
…
<td><button type="button" aria-label="Remove" data-dnd-no-clone @click="emit('remove')">×</button></td>
</tr>
Los fondos no se adivinan
El clon muestra el elemento tal como es. Una fila que no tiene fondo propio, porque se lo pinta
su lista (un <li> en un <ul> blanco, un v-list-item, una fila de tabla), da un clon
transparente. Dale el fondo de la lista en CSS, aquí en las celdas:
<style scoped>
[data-dnd-overlay] .row td {
background: color-mix(in srgb, currentColor 4%, Canvas);
}
</style>
El clon conserva las clases y los atributos del elemento, incluido el atributo de scope de Vue, así que la regla funciona tanto en el estilo scoped de la fila como en una hoja de estilos global. La otra salida es poner un fondo en la propia fila. Los iconos y los mosaicos pensados para flotar se quedan transparentes a propósito.
Dar estilo a la caja
La caja es el elemento con data-dnd-overlay. Es position: fixed, está por encima de todo
(z-index: 9999), tiene pointer-events: none e inert, y su valor dice qué está haciendo:
data-dnd-overlay | Cuándo |
|---|---|
"dragging" | Sigue al puntero. |
"dropping" | Aterriza en su hueco, vuelve a su sitio o espera un onBeforeDrop asíncrono. |
Un aspecto levantado mientras arrastras, que desaparece al aterrizar:
[data-dnd-overlay='dragging'] .row td {
box-shadow: 0 14px 20px -16px rgb(0 0 0 / 0.7);
}
La caja se mueve con transform y recibe su tamaño como width y height: da estilo a lo que
hay dentro, no a la caja. El clon la llena, con width: 100%, height: 100% y transform: none
en su raíz: gíralo o agrándalo con las propiedades rotate y scale, que animan tanto las transiciones como
los keyframes. Recetas en animaciones.
Componentes de overlay
Para mostrar algo distinto del clon, pasa un componente. Se renderiza dentro de la caja y recibe el arrastre como props, que cambian mientras arrastras: dónde está el arrastre, si ese sitio lo acepta, si el drop copia, y el propio drop.
Cargando la demo…
<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Column from './Column.vue';
const columns = ref([
{
id: 'Backlog',
tickets: [
{ id: 't1', title: 'Dark mode toggle', tag: 'UI', approved: false },
{ id: 't2', title: 'Retry failed uploads', tag: 'API', approved: true },
{ id: 't3', title: 'Keyboard shortcuts', tag: 'UI', approved: false },
],
},
{ id: 'Doing', tickets: [{ id: 't4', title: 'Faster search', tag: 'API', approved: true }] },
{ id: 'Done', tickets: [{ id: 't5', title: 'Onboarding tour', tag: 'UI', approved: true }] },
]);
</script>
<template>
<DnDProvider>
<div class="board">
<Column v-for="column in columns" :key="column.id" :column="column" />
</div>
</DnDProvider>
</template>
<style scoped>
.board {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
gap: 12px;
width: 100%;
max-width: 600px;
margin: 0 auto;
}
</style><script setup lang="ts">
import { computed } from 'vue';
import type { DnDStatus, DragItem, DragMode, Over } from '@vue-dnd-kit/core';
const props = defineProps<{ items: DragItem[]; over: Over | null; accepted: boolean; status: DnDStatus; mode: DragMode }>();
const ticket = computed(() => props.items[0]?.item as Ticket);
const state = computed(() => {
if (props.status === 'dropping') return props.over ? 'landed' : 'back'; // a cancel flies back
if (!props.over) return 'free';
return props.accepted ? 'ok' : 'refused';
});
</script>
<template>
<!-- Fills the overlay box, which takes the size of the slot. -->
<div class="ticket-overlay" :data-state="state">
{{ ticket.title }}
<span v-if="mode === 'copy'" class="copy">+ copy</span>
</div>
</template>
<style scoped>
.ticket-overlay { width: 100%; height: 100%; }
.ticket-overlay[data-state='refused'] { background: color-mix(in srgb, #ef4444 8%, Canvas); }
</style>
| Prop | Qué contiene |
|---|---|
active | El arrastre (id, ids, data, type, …). |
items | Todos los ítems arrastrados, empezando por el agarrado: { id, item, data, type }. item es el ítem de la lista en los arrastres ordenables. |
over | El destino bajo el arrastre; mientras aterriza un drop, el destino en el que aterriza. null si no hay nada debajo y mientras un arrastre rechazado o cancelado vuelve a su sitio. El over.id de una lista ordenable es el id de la lista: las columnas de arriba usan sus nombres. |
accepted | Si over acepta el arrastre: tiñe el overlay de rojo cuando no lo hace. |
status | 'dragging', y luego 'dropping' mientras aterriza. |
mode | 'move', 'copy' (Alt mantenido en un arrastre desde una lista con clone, o un arrastre desde una paleta), 'swap', o 'insert' (un elemento arrastrable simple que entra en una lista). |
to | Dónde pondría ahora los ítems un drop: { list, index }, y en un árbol también parentKey y depth. null cuando no los pondría en ninguna lista, p. ej. sobre un destino simple. Consulta dónde iría el drop. |
Las props son OverlayProps; declara solo las que leas. Pasa el componente a lo que se arrastra:
import TicketOverlay from './TicketOverlay.vue';
makeSortableList(listRef, { items: tickets, overlay: TicketOverlay }); // every item of the list
makeSortableItem(el, { item: () => props.ticket, overlay: TicketOverlay }); // this item
makeDraggable(el, { id: 'chip', overlay: TicketOverlay }); // a plain draggable
Un componente se puede pasar tal cual, en un shallowRef para cambiarlo (también a mitad del
arrastre: el overlay cambia al instante), o como una función, que Vue toma como un componente
funcional: overlay: (props) => <TicketOverlay {...props} compact /> en TSX. Nunca lo pongas en
una ref profunda ni en un reactive: Vue haría reactivo el componente y mostraría un aviso.
La raíz de un componente de overlay debe llenar la caja (width: 100%; height: 100%): la caja
toma el tamaño del hueco y lo anima al soltar. Un tamaño fijo en la raíz, en cambio, mantiene ese
tamaño dentro de la caja.
El slot #overlay del provider
Para cada arrastre sin componente propio, el slot del provider renderiza el overlay. Defínelo una vez, junto a la raíz de tu app, y todas las listas y elementos arrastrables de la app lo usarán. Aquí las notas tienen un fantasma propio, y las fotos recurren al clon:
Cargando la demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Moodboard from './Moodboard.vue';
import NoteGhost from './NoteGhost.vue';
</script>
<template>
<DnDProvider>
<Moodboard />
<!--
The overlay of every drag in the app that has none of its own. Notes get a ghost
of their own; for photos the slot renders nothing, so they fall back to the clone.
-->
<template #overlay="{ items, status }">
<NoteGhost v-if="items[0]?.type.includes('note')" :items="items" :status="status" />
</template>
</DnDProvider>
</template><DnDProvider>
<RouterView />
<template #overlay="{ items, status }">
<NoteGhost v-if="items[0]?.type.includes('note')" :items="items" :status="status" />
</template>
</DnDProvider>
El slot recibe las mismas props que un componente de overlay. Cuando no renderiza nada (el
v-if de arriba), el arrastre recurre al clon. Distingue los arrastres por type: el tipo de un
ítem ordenable es el nombre de su grupo, salvo que defina el suyo (makeSortableItem(el, { type })).
Destinos que cambian el overlay
Un destino de drop puede decir qué muestra el overlay mientras un arrastre que acepta está sobre
él. makeDroppable, las listas y los árboles aceptan un overOverlay (el overlay de una lista
o de un árbol es para sus propios ítems). Sobre una carpeta, el chip de los mensajes arrastrados se
convierte en lo que hará el drop:
Cargando la demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Mailbox from './Mailbox.vue';
</script>
<template>
<!-- The mailbox reads the selection with `useDnD()`: it renders inside the provider. -->
<DnDProvider>
<Mailbox />
</DnDProvider>
</template>makeDroppable(el, {
id: () => props.folder.id,
data: () => props.folder,
overOverlay: MovePill,
onDrop: (event) => emit('move', event.active.ids),
});
El componente del destino recibe las mismas props que cualquier componente de overlay, over
entre ellas, y el overlay propio del arrastre (su componente, el slot del provider o el clon) como
su slot por defecto. MovePill deja fuera el slot, así que sustituye al chip:
<script setup lang="ts">
import { computed } from 'vue';
import type { DragItem, Over } from '@vue-dnd-kit/core';
const props = defineProps<{ items: DragItem[]; over: Over | null }>();
// The folder under the drag: its `data`.
const folder = computed(() => props.over?.data as Folder | undefined);
</script>
<template>
<!-- Fills the box like any overlay component. -->
<div class="pill-box">
<span v-if="folder" class="pill">Move {{ items.length }} to {{ folder.name }}</span>
</div>
</template>
Renderiza <slot /> para conservar el overlay propio del arrastre y envolverlo. Es lo que hace
Done en el tablero de componentes de overlay de arriba: sobre Done, un
ticket aprobado conserva su propio overlay, Done le pone un sello, y un ticket de otra columna
recibe confeti al soltarlo.
makeSortableList(listRef, {
items: tickets,
group: { name: 'release', put },
overlay: TicketOverlay, // the tickets' own, in every column
overOverlay: strict ? DoneOverlay : undefined, // Done's, around it
});
<template>
<div class="done">
<slot /> <!-- TicketOverlay -->
<span class="stamp">Shipped</span>
<span v-if="status === 'dropping' && arrives" class="confetti">…</span>
</div>
</template>
- Solo se muestra mientras el destino acepta el arrastre, y se queda mientras el drop aterriza en él. Al salir del destino vuelve el overlay propio del arrastre.
- El cambio se hace con un fundido cruzado. La
overlayTransitiondel provider define cómo:falsecambia al instante, un objeto son las props de un<Transition>alrededor del contenido (name,mode, hooks de JS para GSAP o motion). Sinmode: 'out-in'los dos se ven un momento: dale al que saleposition: absoluteen.<name>-leave-active. - El drop espera al confeti: las animaciones y transiciones CSS que se ejecutan en el overlay mientras aterriza se reproducen hasta el final, durante dos segundos como máximo.
- Un arrastre con
overlay: falseno tiene overlay sobre ningún destino.
Las carpetas también hacen aterrizar la píldora a su manera, dentro del icono de la carpeta: un
destino también puede tener una overDropAnimation. Consulta
animaciones.
Dónde iría el drop: to
to le dice a un overlay dónde pondría ahora los ítems un drop, como lo dirá el to del evento de
drop: la lista y el índice en ella, y en un árbol el padre y la profundidad. Cambia a medida que se
mueve el hueco. Una página de este menú de sitio dice en qué sección entraría, y en qué fila:
Cargando la demo…
<script setup lang="ts">
import { provide, ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import MenuTree from './MenuTree.vue';
import { MENU, site } from './menu';
// Provided above the provider: its overlay renders under it, so the overlay sees it too.
provide(MENU, ref(site()));
</script>
<template>
<DnDProvider>
<MenuTree />
</DnDProvider>
</template><script setup lang="ts">
import { computed } from 'vue';
import type { DragItem, DropDestination } from '@vue-dnd-kit/core';
const props = defineProps<{ items: DragItem[]; to: DropDestination | null }>();
const page = computed(() => props.items[0]!.item as MenuNode);
// `parentKey` is the section the page would go into, `null` at the top level.
const where = computed(() => (props.to ? pathOf(menu.value, props.to.parentKey) : null));
</script>
<template>
<div class="move">
<div class="row">{{ page.title }}</div>
<p v-if="to && where" class="where">Move to <b>{{ where }}</b>, row {{ to.index + 1 }}</p>
</div>
</template>
Fuera de todos los destinos, to sigue siendo el sitio del hueco: por defecto, un drop ahí deja
los ítems donde está el hueco (dropOutside: 'placeholder'). Es null cuando el drop no los
pondría en ninguna lista: sobre un destino simple, sobre una lista que los rechaza, fuera con
dropOutside: 'cancel' o 'remove'.
Qué gana
Para cada arrastre, el primero que se aplique:
- El
overOverlaydel destino de drop (también el de una lista o de un árbol), mientras el arrastre está sobre él y él lo acepta. Lo que se mostraría en otro caso es su slot por defecto. - El
overlaypropio del ítem (makeSortableItem,makeDraggable). - El
overlayde su lista o de su árbol. - El slot
#overlaydel provider, salvo que no renderice nada. - El clon por defecto. En un multi-drag, una columna o un montón de clones: consulta multi-drag.
Si un arrastre tiene overlay o no (overlay: false) se decide cuando empieza. Qué componente se
muestra se lee en vivo: una ref cambiada a mitad del arrastre lo cambia, y lo mismo pasa al entrar
en un destino con overlay propio o al salir de él.
DragPreview: el clon en tu maquetación
DragPreview renderiza el clon que hace la biblioteca de un ítem arrastrado. Un componente de
overlay puede disponer varios, o poner un marco alrededor de uno. Haz clic en libros para
elegirlos y arrastra uno: el overlay abre en abanico los clones reales.
Cargando la demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Shelf from './Shelf.vue';
</script>
<template>
<DnDProvider>
<Shelf />
</DnDProvider>
</template><script setup lang="ts">
import { computed } from 'vue';
import { DragPreview, type DragItem } from '@vue-dnd-kit/core';
const props = defineProps<{ items: DragItem[] }>();
const fan = computed(() =>
props.items.slice(0, 4).map((entry, index) => ({
entry,
angle: index === 0 ? 0 : (index % 2 ? -1 : 1) * Math.ceil(index / 2) * 10,
})),
);
</script>
<template>
<div class="fan">
<div v-for="{ entry, angle } in fan" :key="entry.id" class="card" :style="{ transform: `rotate(${angle}deg)` }">
<DragPreview :id="entry.id" class="face" :style="{ width: '100%', height: '100%' }">
<div class="blank" /> <!-- an item without a clone -->
</DragPreview>
</div>
<span v-if="items.length > 1" class="count">{{ items.length }}</span>
</div>
</template>
- Hay clones para el ítem agarrado y los demás junto a él en el orden del drop, tantos como
indique el
overlayPreviewsdel provider (5 por defecto,'all'para todos), si están renderizados al agarrar. Para cualquier otro ítem,DragPreviewrenderiza su slot por defecto. - Una vista previa tiene el tamaño de su ítem al agarrar, puesto en línea. Ajusta su tamaño con
style, que se transmite (una clase no puede sobrescribirlo), como arriba. - En un multi-drag ordenable (
multiple: 'group'o'gaps'), cada vista previa aterriza al soltar en el hueco de su propio ítem. Gira sus envoltorios contransform(no con la propiedadrotate): el aterrizaje lo deshace por el camino. Pon el aspecto del ítem (esquinas, sombra) en la propia vista previa, como haceclass="face": los envoltorios se quedan en la caja.
La guía de multi-drag tiene un overlay que muestra un contador y los títulos en lugar de los clones.
Tamaño: overlayFit
Sobre una lista de otros tamaños, la caja toma el tamaño del hueco donde aterrizará. El
overlayFit del provider lo cambia; la página de DnDProvider tiene una
demo con una lista estrecha y otra ancha.
overlayFit (provider) | La caja |
|---|---|
'slot' (por defecto) | Toma el tamaño del hueco donde aterrizará, ajustándose a él con suavidad. Sobre la lista ancha, se ensancha. |
'source' | Mantiene el tamaño del elemento arrastrado. |
Con 'slot', la caja cambia de tamaño alrededor del puntero, que se queda en la misma fracción de
la caja. El contenido nunca se escala: se vuelve a maquetar al nuevo tamaño, así que una fila
flex como las tareas de esa demo se ve bien a cualquier anchura. Un hueco con un estilo mucho más
pequeño que el ítem, como una línea fina, no encoge la caja en ese eje.
Un drop hace lo mismo con cualquiera de los dos ajustes: la caja vuela hasta el elemento de llegada y se anima hasta su tamaño, y el contenido se vuelve a maquetar por el camino. Cuando un ítem se ve distinto en dos listas (un chip compacto en una paleta, una tarjeta completa en un tablero), el clon del origen se vuelve a maquetar al tamaño del destino y puede verse aplastado o estirado. Dos formas de que se vea bien:
- Dale al ítem una maquetación que funcione en los dos tamaños, como las filas de tareas, o el mismo tamaño en todas las listas en las que puede aterrizar.
- Renderiza el aspecto del destino en un componente de overlay. Recibe
over, así que puede cambiar al aspecto de la lista sobre la que está, y llena la caja a cualquier tamaño.
Dónde se renderiza: overlayTo
El provider teletransporta el overlay a body. Un <dialog> modal (abierto con showModal())
está en la top layer, por encima de todo lo que hay en body, así que un overlay ahí quedaría
oculto debajo. Apunta overlayTo a un selector o a un elemento dentro del dialog. La
página de DnDProvider tiene un dialog para
probarlo con y sin él:
<script setup lang="ts">
const dialog = useTemplateRef<HTMLDialogElement>('dialog');
const open = ref(false);
// While the dialog is open, the overlay renders inside it.
const overlayTo = computed(() => (open.value ? '#columns-dialog' : 'body'));
const show = () => {
dialog.value?.showModal();
open.value = true;
};
</script>
<template>
<DnDProvider :overlay-to="overlayTo">
<button type="button" @click="show">Edit columns</button>
<dialog id="columns-dialog" ref="dialog" @close="open = false">
<ColumnList v-model:columns="columns" />
</dialog>
</DnDProvider>
</template>
- El destino debe estar en la página cuando el provider renderiza el overlay, después de
montarse. Un
<dialog>cerrado lo está: solo está oculto. Vuelve abodymientras está cerrado, para que se vea el overlay de los demás arrastres. - La caja es
position: fixed. Untransform,filterocontainen el destino o en sus ancestros lo convierte en el marco de la caja en lugar del viewport, así que no los pongas en el dialog. - Los estilos del clon se calculan para el sitio donde se muestra: en un shadow root, apunta
overlayTodentro de él y el clon conserva los estilos del shadow root.
Sin overlay
makeDraggable(el, { overlay: false }) no tiene caja: el propio elemento se mueve con el
transform devuelto.
const { transform } = makeDraggable(el, { overlay: false });
const style = computed(() =>
transform.value ? { translate: `${transform.value.x}px ${transform.value.y}px` } : undefined,
);
Consulta arrastrar y soltar. Cómo aterriza un
arrastre con caja lo decide su dropAnimation: la del destino de drop (overDropAnimation), la del ítem (o la de su
lista) o la del provider. Consulta animaciones.