Documentación/Guías
El provider
DnDProvider gestiona todos los arrastres que hay debajo. Encuentra el destino, inicia los arrastres, desplaza y cuenta a useDnD y useDnDMonitor todo lo que pasa.
Esta página la ha traducido una IA y puede contener imprecisiones. Leer el original en inglés
Un provider para la app
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
</script>
<template>
<DnDProvider>
<RouterView />
</DnDProvider>
</template>
Pon un DnDProvider arriba del todo, en App.vue o en un layout, y olvídate de él. Cada
composable (makeSortableList, makeDraggable, useDnD…) busca el provider más cercano por
encima del componente en el que se ejecuta.
<DnDProvider> lanza
No <DnDProvider> found above this component: el inject de Vue solo ve a los ancestros.Lo que gestiona
| Parte | Lo que hace el provider |
|---|---|
| Entrada | Convierte la entrada de puntero, táctil y de teclado en arrastres (sensors, activation), un arrastre a la vez. |
| Encontrar el destino | Hace la prueba de colisión con los destinos de drop bajo el arrastre (collision) y se lo comunica. |
| El overlay | Renderiza la caja que sigue al puntero, teletransportada a body, y la hace aterrizar al soltar. |
| La selección | Un único conjunto de ids seleccionados, compartido por todas las listas y elementos arrastrables que tiene debajo. |
| Desplazamiento automático | Desplaza los contenedores y la ventana cuando un arrastre se acerca a sus bordes. |
| La región viva | Anuncia a los lectores de pantalla los agarres, movimientos, drops y cancelaciones. |
No renderiza nada más: tu marcado se queda como está.
Las opciones de un vistazo
Cada opción tiene un valor por defecto que funciona, así que el provider normalmente no recibe
ninguna. El provider lee sus props en el momento en que las necesita: las demos de abajo cambian
collision, activation y autoScroll en vivo.
| Prop | Por defecto | Qué ajusta |
|---|---|---|
collision | pointerFirst | Cómo se elige el destino bajo el arrastre. Más abajo. |
activation | ratón 4 px, táctil 200 ms | Cuándo una pulsación se convierte en arrastre, según el tipo de puntero. Más abajo. |
autoScroll | activado | Desplazamiento en los bordes: { threshold, maxSpeed, acceleration } o false. Más abajo. |
haptics | una vibración corta al agarrar con el dedo | Vibraciones de los arrastres táctiles, o false. Más abajo. |
copyModifier | 'Alt' | Tecla que, mantenida durante el arrastre, convierte un movimiento en una copia. |
modifiers | [] | Reglas de movimiento para todos los arrastres: consulta las restricciones. |
animation / dropAnimation | activado | Los ítems que se deslizan al ordenar / el aterrizaje del overlay: consulta las animaciones. |
animateChanges | desactivado | Valor por defecto de listas y árboles: animar los cambios de datos que no hizo ningún arrastre. |
keyboard | Space / Enter, Escape | Teclas y paso del teclado; false desactiva los arrastres con teclado. Consulta la accesibilidad. |
sensors | puntero + teclado | Lo que convierte la entrada en arrastres: consulta los sensores. |
overlayTo | 'body' | Adónde se teletransporta el overlay. |
overlayFit | 'slot' | Tamaño del overlay: el del hueco donde aterrizará, o el del origen. Consulta el overlay. |
overlayTransition | un fundido cruzado | Cómo cambia el overlay de un componente a otro durante el arrastre: false, o props de <Transition>. Consulta los destinos que cambian el overlay. |
overlayClass | — | Clases para el contenido del overlay ([data-dnd-overlay-content]), el elemento que se anima entero: un levantar al agarrar, un asentar al aterrizar. Consulta el overlay: levantar, inclinar, asentar. |
overlayPreviews | 5 | Multi-drag: cuántos ítems arrastrados reciben un clon al agarrar, o 'all'. Consulta el overlay por defecto. |
announcements | integrados | Textos para lectores de pantalla, o false. |
screenReaderInstructions | integrado | El texto al que apunta cada elemento arrastrable con aria-describedby. |
defaultStyles | true | Inyecta CSS de especificidad cero para los estados de arrastre: consulta los estilos. |
La lista completa, con todas las formas, está en la API de DnDProvider.
El provider también emite drag-start, drag-move, drag-over, drag-end, drag-cancel,
drag-resume y drag-settle, los mismos eventos que escucha useDnDMonitor.
Leer el arrastre: useDnD()
Cargando la demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Tasks from './Tasks.vue';
import StatePanel from './StatePanel.vue';
</script>
<template>
<!-- One provider: the list drags, the panel reads the drag. Neither imports the other. -->
<DnDProvider>
<div class="demo">
<Tasks />
<StatePanel />
</div>
</DnDProvider>
</template>
<style scoped>
.demo {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: center;
gap: 24px;
}
</style>useDnD() devuelve el arrastre como estado reactivo, en cualquier componente bajo el provider:
import { useDnD } from '@vue-dnd-kit/core';
const { status, active, over, pointer, activation, isAutoScrolling, selection, cancel } = useDnD();
| Campo | Qué contiene |
|---|---|
status | 'idle', 'activating' (una pulsación que espera su activación), 'dragging', 'dropping' (no hay ningún arrastre en curso, pero un overlay todavía aterriza, o decide un onBeforeDrop asíncrono), 'selecting' (un rectángulo de selección). |
active | Lo que se arrastra: id, ids (todos los ids arrastrados, empezando por el que se agarró), items, data, type, element, rect, sensor. null en reposo. |
over | El destino bajo el arrastre: id, data, element, rect y accepted. null si no hay nada debajo. |
pointer | El puntero en coordenadas del viewport, después de los modificadores. |
activation.progress | De 0 a 1 mientras una pulsación espera su activación: dibuja un anillo con él. |
isAutoScrolling | El desplazamiento automático está moviendo algo. |
selection | La selección del provider: ids, has, add, delete, toggle, set, clear. Consulta multi-drag. |
cancel() | Cancela el arrastre en curso (los ítems vuelven a su sitio), una pulsación que está esperando o un rectángulo de selección. |
<script setup lang="ts">
import { computed } from 'vue';
import { useDnD } from '@vue-dnd-kit/core';
const { status, over } = useDnD();
const refused = computed(() => status.value === 'dragging' && over.value?.accepted === false);
</script>
<template>
<p v-if="refused" role="status">This list doesn't take that.</p>
</template>
En cuanto se decide un drop, active, over y pointer valen null, incluso mientras el
overlay sigue aterrizando (status vale 'dropping'). La UI derivada de ellos nunca
muestra el arrastre anterior junto a los datos nuevos. Mientras un onBeforeDrop asíncrono
espera, el estado se queda como estaba al soltar.
El siguiente arrastre no espera a ningún aterrizaje: empieza enseguida, y status vale
'dragging' mientras el último overlay aterriza a su alrededor. Una pulsación sobre un overlay
que está aterrizando lo atrapa en pleno vuelo: el arrastre continúa desde donde está el overlay.
onDragSettle avisa cuando el overlay de un arrastre ha llegado a su sitio.
Escuchar todos los arrastres: useDnDMonitor()
import { useDnDMonitor } from '@vue-dnd-kit/core';
useDnDMonitor({
onDragStart: ({ active }) => {},
onDragMove: ({ pointer, delta }) => {},
onDragOver: ({ over }) => {}, // the target changed
onBeforeDrop: (e) => 'accept', // or 'reject', 'continue', some ids; may be async
onDragEnd: ({ active, over, dropped, accepted, position }) => {},
onDragCancel: ({ active }) => {},
onDragResume: ({ active }) => {}, // onBeforeDrop answered 'continue'
onDragSettle: ({ active, over }) => {}, // its overlay has landed
});
Un monitor escucha todos los arrastres bajo su provider, empiecen donde empiecen: el panel de arriba registra los arrastres de la lista sin importarla. Se detiene cuando se desmonta el componente que lo llamó.
| Evento | Cuándo |
|---|---|
onDragStart | Empieza un arrastre. |
onDragMove | El arrastre se mueve. |
onDragOver | Cambia el destino bajo el arrastre. |
onBeforeDrop | Se suelta el arrastre. Su respuesta decide el drop: consulta decidir un drop. |
onDragEnd | El drop se completó. Las listas ordenables ya ejecutaron su onDrop y escribieron el nuevo orden. dropped es false cuando terminó sin ningún destino debajo. |
onDragCancel | Escape, un drop sobre un destino que lo rechaza, o un onBeforeDrop que respondió 'reject'. |
onDragResume | onBeforeDrop respondió 'continue': no se soltó nada y el arrastre continúa. |
onDragSettle | El overlay del arrastre ha aterrizado (o ha vuelto a su sitio, o lo atrapó el siguiente arrastre), con el evento que recibió onDragEnd u onDragCancel. Mientras tanto pueden haber empezado otros arrastres. |
Cada evento lleva active, over, pointer, delta (el desplazamiento desde el inicio),
position (dónde queda la caja arrastrada dentro de over, consulta
arrastrar y soltar) y, para los ítems
de una lista ordenable, source. Las formas completas están en la API de eventos.
onDragEnd es el sitio para el trabajo «después del drop»: guardar, limpiar la selección,
analítica. El onDrop propio de una lista se ejecuta antes de su escritura, así que todavía
ve el orden anterior.Encontrar el destino
Elige una función y arrastra el pin o la tarjeta sobre los destinos. Las insignias muestran cómo los clasifica la función en este frame; la verde es el destino.
Cargando la demo…
<script setup lang="ts">
import { computed, ref, shallowRef } from 'vue';
import {
DnDProvider,
closestCenter,
closestCorners,
pointerFirst,
pointerWithin,
rectIntersection,
type CollisionDetection,
type DnDEndEvent,
type Key,
} from '@vue-dnd-kit/core';
import Desk from './Desk.vue';
import { magnet } from './magnet';
const DETECTORS: Record<string, CollisionDetection> = {
pointerFirst,
pointerWithin,
rectIntersection,
closestCenter,
closestCorners,
magnet,
};
const names = Object.keys(DETECTORS);
const mode = ref('pointerFirst');
// The ranking of the current frame, best first: a wrapper around the chosen function
// records it, so the targets can show their rank.
const ranked = shallowRef<Key[]>([]);
const collision = computed<CollisionDetection>(() => {
const detect = DETECTORS[mode.value]!;
return (args) => (ranked.value = detect(args));
});
const result = ref('Drag the pin or the card over the targets');
const onEnd = ({ over }: DnDEndEvent) => {
ranked.value = [];
result.value = over ? `Dropped on ${over.id}` : 'Dropped on nothing';
};
const onCancel = () => {
ranked.value = [];
result.value = 'Cancelled';
};
</script>
<template>
<DnDProvider :collision="collision" @drag-end="onEnd" @drag-cancel="onCancel">
<div class="lab">
<div class="modes" role="radiogroup" aria-label="Collision detection">
<label v-for="name in names" :key="name" :class="{ on: mode === name }">
<input v-model="mode" type="radio" :value="name" />
{{ name }}
</label>
</div>
<code class="call"><DnDProvider :collision="{{ mode }}"></code>
<Desk :ranked="ranked" />
<p class="result" aria-live="polite">{{ result }}</p>
</div>
</DnDProvider>
</template>
<style scoped>
.lab {
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
}
.modes {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2px;
max-width: 100%;
padding: 3px;
border-radius: 9px;
background: color-mix(in srgb, currentColor 7%, transparent);
}
.modes label {
padding: 5px 9px;
border-radius: 6px;
font: 12px/1.2 ui-monospace, Menlo, Consolas, monospace;
cursor: pointer;
}
.modes label.on {
background: Canvas;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
font-weight: 600;
}
.modes label:has(input:focus-visible) {
outline: 2px solid #42b883;
}
.modes input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.call {
max-width: 100%;
padding: 6px 10px;
overflow-x: auto;
border-radius: 8px;
background: color-mix(in srgb, currentColor 6%, transparent);
font: 11px/1.4 ui-monospace, Menlo, Consolas, monospace;
white-space: nowrap;
color: #42b883;
}
.result {
margin: 0;
font: 11px/1.4 ui-monospace, Menlo, Consolas, monospace;
opacity: 0.6;
}
</style>En cada frame de un arrastre, el provider le da a collision los destinos de drop en juego, y
ella devuelve sus ids, el mejor primero. La biblioteca trae cinco funciones:
| Función | Clasifica | Buena para |
|---|---|---|
pointerFirst | Lo que está bajo el puntero; si no hay nada, lo que solapa la caja arrastrada. | El valor por defecto: precisa bajo el puntero, tolerante en los huecos. |
pointerWithin | Solo lo que está bajo el puntero, el más interior primero. | Destinos precisos: un hueco no suelta en ningún sitio. |
rectIntersection | Cada destino que solapa la caja arrastrada: mayor priority, luego anidamiento más profundo, luego mayor solapamiento. | Ítems grandes sobre destinos pequeños, cuando el puntero está en el borde del ítem. |
closestCenter | Cada destino, por la distancia del centro de la caja al suyo. | Siempre gana algo, incluso lejos: cuadrículas y listas de ítems del mismo tamaño. |
closestCorners | Cada destino, por las distancias entre las cuatro esquinas. | Columnas una al lado de otra (kanban), destinos de alturas muy distintas. |
Bajo el puntero, la prueba de colisión respeta lo que ve el usuario: de los destinos superpuestos
solo se acierta el de encima (front, nunca back donde se solapan), y de los anidados el más
interior va primero (subfolder, luego folder).
Una función propia
import type { CollisionDetection, Coordinates, Rect } from '@vue-dnd-kit/core';
/** How far the point is from the rect: 0 inside it. */
const gap = (rect: Rect, point: Coordinates) =>
Math.hypot(
Math.max(rect.left - point.x, 0, point.x - rect.right),
Math.max(rect.top - point.y, 0, point.y - rect.bottom),
);
/** What is under the pointer; in a gap, the nearest target within 40px. */
export const magnet: CollisionDetection = ({ hits, pointer, candidates }) => {
if (hits.length) return hits;
return candidates
.filter((candidate) => gap(candidate.rect, pointer) <= 40)
.sort((a, b) => gap(a.rect, pointer) - gap(b.rect, pointer))
.map((candidate) => candidate.id);
};
| Argumento | Qué contiene |
|---|---|
pointer | El puntero, después de los modificadores. |
overlayRect | La caja arrastrada tal como se muestra, null sin overlay. |
candidates | Cada destino en juego: id, rect (su parte visible, recortada por los contenedores desplazables), priority, depth (0 arriba del todo, mayor cuando está anidado). |
hits | Los destinos bajo el puntero, según la prueba de colisión, el mejor primero. |
Devuelve un array vacío para «ningún destino». Los destinos desactivados, los destinos dentro de
los ítems arrastrados y los destinos ocultos por sus contenedores desplazables nunca llegan a la
función. Un destino que rechaza el arrastre sigue compitiendo: sobre él, over.accepted es
false y el drop se rechaza (un destino dentro de un ítem ordenable se aparta en su lugar, para
que la lista ordene por encima). La demo envuelve la función elegida para registrar su
clasificación: (args) => (ranked.value = detect(args)).
Cuándo una pulsación se convierte en arrastre
Una pulsación no arrastra de inmediato. activation define, por tipo de puntero (mouse,
touch, pen), qué espera: una distance en px, un delay en ms (con una tolerance a partir
de la cual la pulsación se abandona, para que la página pueda desplazarse), o lo que ocurra
primero de los dos.
<DnDProvider :activation="{ mouse: { distance: 8 }, touch: { delay: 300, tolerance: 8 } }">
| Puntero | Por defecto |
|---|---|
mouse, pen | { distance: 4 }: un clic sigue siendo un clic. Una pulsación de ratón sobre un asa (handle) empieza de inmediato, y sigue siendo un clic si no se mueve. |
touch | { delay: 200, tolerance: 5 }: una pulsación larga, para que las listas sigan desplazándose bajo el dedo. |
Un tipo que omitas conserva su valor por defecto. La activation propia de un elemento
arrastrable gana a la de un área de arrastre, que gana a la del
provider. Mientras una pulsación espera, useDnD().status es 'activating' y
activation.progress va de 0 a 1; Escape o useDnD().cancel() la abandonan. Prueba las
restricciones en vivo y dibuja un anillo de pulsación larga a partir del progreso en
sensores.
Desplazamiento automático
Arrastra un canal hacia arriba o hacia abajo de la lista. Las franjas verdes son las zonas de borde: cuanto más te adentras en una, más rápido se desplaza la lista.
Cargando la demo…
<script setup lang="ts">
import { computed, ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Channels from './Channels.vue';
const OPTIONS = {
threshold: [0.1, 0.2, 0.4],
maxSpeed: [10, 25, 50],
acceleration: [4, 10, 25],
} as const;
const on = ref(true);
// The defaults: an edge zone of 20% of the container (24 to 96px), 10px a frame at the
// edge, up to 25px a frame past it.
const options = ref({ threshold: 0.2, maxSpeed: 25, acceleration: 10 });
const autoScroll = computed(() => (on.value ? options.value : false));
const call = computed(() =>
on.value
? `:auto-scroll="{ threshold: ${options.value.threshold}, maxSpeed: ${options.value.maxSpeed}, acceleration: ${options.value.acceleration} }"`
: ':auto-scroll="false"',
);
</script>
<template>
<DnDProvider :auto-scroll="autoScroll">
<div class="demo">
<div class="switches">
<label class="switch">
<input v-model="on" type="checkbox" />
autoScroll
</label>
<fieldset v-for="(values, name) in OPTIONS" :key="name" :disabled="!on">
<legend>{{ name }}</legend>
<label v-for="value in values" :key="value" :class="{ on: options[name] === value }">
<input v-model="options[name]" type="radio" :value="value" />
{{ value }}
</label>
</fieldset>
</div>
<code class="call">{{ call }}</code>
<Channels :threshold="on ? options.threshold : 0" />
</div>
</DnDProvider>
</template>
<style scoped>
.demo {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.switches {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px 14px;
}
fieldset {
display: inline-flex;
margin: 0;
padding: 3px;
border: 0;
border-radius: 9px;
background: color-mix(in srgb, currentColor 7%, transparent);
}
fieldset:disabled {
opacity: 0.4;
}
legend {
float: left;
align-self: center;
padding: 0 8px 0 6px;
font: 10px/1 ui-monospace, Menlo, Consolas, monospace;
opacity: 0.55;
}
fieldset label {
padding: 5px 9px;
border-radius: 6px;
font: 12px/1.2 ui-monospace, Menlo, Consolas, monospace;
cursor: pointer;
}
fieldset label.on {
background: Canvas;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
font-weight: 600;
}
fieldset label:has(input:focus-visible),
.switch:has(input:focus-visible) {
outline: 2px solid #42b883;
}
fieldset input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.switch {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border-radius: 9px;
background: color-mix(in srgb, currentColor 7%, transparent);
font: 12px/1.2 ui-monospace, Menlo, Consolas, monospace;
cursor: pointer;
}
.switch input {
accent-color: #42b883;
}
.call {
max-width: 100%;
padding: 6px 10px;
overflow-x: auto;
border-radius: 8px;
background: color-mix(in srgb, currentColor 6%, transparent);
font: 11px/1.4 ui-monospace, Menlo, Consolas, monospace;
white-space: nowrap;
color: #42b883;
}
</style>Cualquier elemento con overflow: auto o scroll se desplaza solo, y la ventana también. El
provider desplaza primero el contenedor bajo el puntero, luego sus ancestros y luego la ventana.
| Opción | Por defecto | Qué ajusta |
|---|---|---|
threshold | 0.2 | La zona de borde, como fracción del tamaño del contenedor, entre 24 y 96 px. |
acceleration | 10 | La velocidad justo en el borde, en px por frame. Crece con el cuadrado de la profundidad dentro de la zona, así que el arranque es lento y preciso. |
maxSpeed | 25 | La velocidad máxima. Más allá del borde (el puntero fuera del contenedor), la velocidad sigue creciendo hasta ella. |
:auto-scroll="false" lo desactiva. Una dirección solo se desplaza cuando el puntero se ha movido
hacia ella, así que un ítem agarrado cerca de un borde no se desplaza solo. Las listas, los
árboles y las áreas de arrastre aceptan su propio autoScroll para su
contenedor:
makeSortableList(list, { items: channels, autoScroll: { maxSpeed: 10 } });
Los arrastres con teclado nunca usan el desplazamiento automático: en su lugar desplazan el ítem
hasta hacerlo visible. isAutoScrolling (de useDnD(), o de un área de arrastre) es true
mientras algo se desplaza.
Vibración y la tecla de copia
Un arrastre táctil vibra brevemente al empezar. :haptics="false" lo desactiva; un objeto ajusta
cada vibración:
<DnDProvider :haptics="{ start: true, enter: 10, drop: 10, refuse: [20, 40, 20] }">
| Clave | Vibra cuando | Por defecto |
|---|---|---|
start | La pulsación larga se convierte en arrastre. | true (10 ms) |
enter | El arrastre entra en un destino que lo acepta. | desactivado |
drop | El drop se completa. | desactivado |
refuse | El drop se rechaza y los ítems vuelven a su sitio. | desactivado |
Cada una acepta true (el patrón por defecto), una duración en ms, un patrón de encendido y
apagado, o false. Solo vibran los arrastres iniciados con el dedo, y Safari en iOS no puede
vibrar en absoluto.
copyModifier es la tecla que, mantenida durante un arrastre entre listas, convierte el
movimiento en una copia: 'Alt' por defecto, cualquier KeyboardEvent.key, o null para
ninguna. La lista de la que salen los ítems necesita una función clone; consulta
listas que comparten ítems.
Varios providers
La mayoría de las apps necesitan exactamente uno. Pon la lista de deseos bajo su propio provider e intenta mover un libro al otro lado: no puede salir de su provider, y el monitor exterior deja de oír la lista de deseos.
Cargando la demo…
<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Shelf from './Shelf.vue';
import EventLog from './EventLog.vue';
const reading = ref([
{ id: 'dune', title: 'Dune', hue: 32 },
{ id: 'emma', title: 'Emma', hue: 330 },
{ id: 'ulysses', title: 'Ulysses', hue: 210 },
]);
const wishlist = ref([
{ id: 'beloved', title: 'Beloved', hue: 140 },
{ id: 'solaris', title: 'Solaris', hue: 260 },
]);
const separate = ref(false);
</script>
<template>
<DnDProvider>
<div class="demo">
<div class="modes" role="radiogroup" aria-label="Wishlist">
<label :class="{ on: !separate }"><input v-model="separate" type="radio" :value="false" />one provider</label>
<label :class="{ on: separate }"><input v-model="separate" type="radio" :value="true" />wishlist in its own</label>
</div>
<div class="columns">
<div class="column">
<Shelf id="reading" v-model="reading" title="Reading" />
<EventLog title="outer monitor" />
</div>
<!-- A provider inside a provider: its items belong to it, and drags never cross. -->
<DnDProvider v-if="separate">
<div class="column island">
<span class="tag"><DnDProvider></span>
<Shelf id="wishlist" v-model="wishlist" title="Wishlist" />
<EventLog title="inner monitor" />
</div>
</DnDProvider>
<div v-else class="column">
<Shelf id="wishlist" v-model="wishlist" title="Wishlist" />
</div>
</div>
</div>
</DnDProvider>
</template>
<style scoped>
.demo {
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
}
.modes {
display: inline-flex;
padding: 3px;
border-radius: 9px;
background: color-mix(in srgb, currentColor 7%, transparent);
}
.modes label {
padding: 5px 10px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
}
.modes label.on {
background: Canvas;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
font-weight: 600;
}
.modes label:has(input:focus-visible) {
outline: 2px solid #42b883;
}
.modes input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.columns {
display: grid;
grid-template-columns: repeat(2, minmax(0, 230px));
gap: 16px;
max-width: 100%;
}
.column {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.island {
position: relative;
margin: -10px;
padding: 10px;
border: 1.5px dashed color-mix(in srgb, #42b883 60%, transparent);
border-radius: 16px;
}
.tag {
position: absolute;
top: -9px;
right: 14px;
padding: 0 6px;
background: Canvas;
font: 10px/18px ui-monospace, Menlo, Consolas, monospace;
color: #42b883;
}
</style><DnDProvider>
<Shelf v-model="reading" />
<DnDProvider>
<Shelf v-model="wishlist" />
<EventLog /> <!-- hears the inner provider's drags only -->
</DnDProvider>
</DnDProvider>
- Los providers son independientes: un arrastre nunca pasa de uno a otro.
- Se permiten providers anidados. Gana el dueño más cercano: un ítem pertenece al provider más cercano por encima de él, y el provider exterior lo ignora.
- Cada provider tiene su propia selección, su overlay, su región viva y sus monitores.
Un segundo provider solo tiene sentido para una parte de la página que necesita sus propios ajustes a nivel de provider (colisión, teclas del teclado, anuncios) y cuyos ítems nunca salen de ella. Si los ítems no pueden moverse entre dos partes de tu página, comprueba que no estén bajo dos providers.
makeDragArea(board, { modifiers: [snapToGrid(20)], activation: { touch: { delay: 400 } } });
Una app montada en otro sitio, dentro de un shadow root o como una app de Vue aparte, puede unirse
a un provider existente: pasa useDnDContext() a provideDnDContext(app, context) antes de
montarla. La prueba de colisión y el overlay funcionan a través de los shadow roots.
SSR y Nuxt
El provider es seguro para SSR. Las listas y los ítems se renderizan en el servidor como cualquier
otro marcado. El overlay y la región viva se teletransportan y se montan solo en el cliente, así
que nunca causan un error de hidratación. Los ids por defecto vienen de useId() de Vue y
coinciden entre servidor y cliente.
En Nuxt, el módulo registra DnDProvider globalmente e importa automáticamente los composables:
consulta la instalación.