Documentação/Guias

O provider

DnDProvider controla todo arraste abaixo dele. Ele encontra o alvo, inicia arrastes, rola a página e conta ao useDnD e ao useDnDMonitor tudo o que acontece.

Editar esta página

Esta página foi traduzida por IA e pode conter imprecisões. Ler o original em inglês

Um provider para o app

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

<template>
  <DnDProvider>
    <RouterView />
  </DnDProvider>
</template>

Coloque um DnDProvider lá no alto, no App.vue ou em um layout, e esqueça dele. Todo composable (makeSortableList, makeDraggable, useDnD…) procura o provider mais próximo acima do componente em que roda.

Um composable precisa rodar em um componente renderizado dentro do provider. Chamá-lo no componente cujo próprio template contém <DnDProvider> lança No <DnDProvider> found above this component: o inject do Vue só enxerga ancestrais.

O que ele controla

ParteO que o provider faz
EntradaTransforma a entrada por ponteiro, toque e teclado em arrastes (sensors, activation), um arraste por vez.
Encontrar o alvoTesta quais alvos de drop estão sob o arraste (collision) e avisa a eles.
O overlayRenderiza a caixa que segue o ponteiro, teleportada para o body, e a faz pousar no drop.
A seleçãoUm único conjunto de ids selecionados, compartilhado por todas as listas e draggables abaixo dele.
Rolagem automáticaRola os contêineres e a janela quando um arraste chega perto das bordas deles.
A live regionAnuncia aos leitores de tela os inícios de arraste, os movimentos, os drops e os cancelamentos.

Ele não renderiza mais nada: a sua marcação fica como está.

Opções em resumo

Toda opção tem um padrão que funciona, então em geral o provider não recebe nenhuma. O provider lê as props quando precisa delas: as demos abaixo trocam collision, activation e autoScroll ao vivo.

PropPadrãoO que define
collisionpointerFirstComo o alvo sob o arraste é escolhido. Abaixo.
activationmouse 4 px, toque 200 msQuando uma pressão vira arraste, por tipo de ponteiro. Abaixo.
autoScrollligadoRolagem nas bordas: { threshold, maxSpeed, acceleration } ou false. Abaixo.
hapticsuma vibração curta ao pegar por toqueVibrações para arrastes por toque, ou false. Abaixo.
copyModifier'Alt'Tecla que, segurada durante o arraste, transforma um movimento em cópia.
modifiers[]Regras de movimento para todo arraste: veja restrições.
animation / dropAnimationligadoItens deslizando durante a ordenação / o overlay pousando: veja animações.
animateChangesdesligadoPadrão para listas e árvores: animar mudanças nos dados que nenhum arraste fez.
keyboardSpace / Enter, EscapeTeclas e passo do teclado; false desliga os arrastes por teclado. Veja acessibilidade.
sensorsponteiro + tecladoO que transforma a entrada em arrastes: veja sensores.
overlayTo'body'Para onde o overlay é teleportado.
overlayFit'slot'Tamanho do overlay: o da posição em que ele vai pousar, ou o da origem. Veja o overlay.
overlayTransitionum crossfadeComo o overlay muda de um componente para outro durante o arraste: false, ou props de <Transition>. Veja alvos que mudam o overlay.
overlayClass—Classes para o conteúdo do overlay ([data-dnd-overlay-content]), o elemento a animar como um todo: erguer quando o arraste começa, assentar quando ele pousa. Veja o overlay: erguer, inclinar, assentar.
overlayPreviews5Multi-drag: quantos itens arrastados ganham um clone no início do arraste, ou 'all'. Veja o overlay padrão.
announcementsembutidosTextos para leitores de tela, ou false.
screenReaderInstructionsembutidoO texto que todo draggable aponta com aria-describedby.
defaultStylestrueInjeta CSS de especificidade zero para os estados do arraste: veja estilização.

A lista completa, com todas as formas, está na API do DnDProvider. O provider também emite drag-start, drag-move, drag-over, drag-end, drag-cancel, drag-resume e drag-settle, os mesmos eventos que o useDnDMonitor ouve.

Ler o arraste: useDnD()

Live

Carregando a 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() retorna o arraste como estado reativo, em qualquer componente abaixo do provider:

import { useDnD } from '@vue-dnd-kit/core';

const { status, active, over, pointer, activation, isAutoScrolling, selection, cancel } = useDnD();
CampoO que contém
status'idle', 'activating' (uma pressão esperando a ativação), 'dragging', 'dropping' (nenhum arraste em andamento, mas um overlay ainda está pousando, ou um onBeforeDrop assíncrono está decidindo), 'selecting' (um retângulo de seleção).
activeO que está sendo arrastado: id, ids (todos os ids arrastados, o pego primeiro), items, data, type, element, rect, sensor. null quando parado.
overO alvo sob o arraste: id, data, element, rect e accepted. null sobre nada.
pointerO ponteiro em coordenadas da viewport, depois dos modificadores.
activation.progressDe 0 a 1 enquanto uma pressão espera a ativação: desenhe um anel com ele.
isAutoScrollingA rolagem automática está movendo alguma coisa.
selectionA seleção do provider: ids, has, add, delete, toggle, set, clear. Veja multi-drag.
cancel()Cancela o arraste em andamento (os itens voltam ao lugar), uma pressão carregando ou um retângulo de seleção.
DropHint.vue
<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>

Assim que um drop é decidido, active, over e pointer passam a ser null, mesmo enquanto o overlay ainda está pousando (status vale 'dropping'). Uma UI derivada deles nunca mostra o arraste antigo ao lado dos dados novos. Enquanto um onBeforeDrop assíncrono espera, o estado fica como estava no momento de soltar.

O próximo arraste não espera um pouso: ele começa na hora, e status vale 'dragging' enquanto o último overlay pousa ao redor dele. Uma pressão sobre um overlay que está pousando o pega em pleno voo: o arraste continua de onde o overlay está. onDragSettle avisa quando o overlay de um arraste chegou ao seu lugar.

Ouvir todo arraste: 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
});

Um monitor ouve todo arraste abaixo do seu provider, onde quer que ele comece: o painel acima registra os arrastes da lista sem importá-la. Ele para quando o componente que o chamou é desmontado.

EventoQuando
onDragStartUm arraste começa.
onDragMoveO arraste se move.
onDragOverO alvo sob o arraste muda.
onBeforeDropO arraste é solto. A resposta decide o drop: veja decidindo um drop.
onDragEndO drop foi concluído. As listas ordenáveis já rodaram o onDrop delas e gravaram a nova ordem. dropped é false quando o arraste terminou fora de qualquer alvo.
onDragCancelEscape, um drop em um alvo que o recusa, ou um onBeforeDrop que respondeu 'reject'.
onDragResumeonBeforeDrop respondeu 'continue': nada foi solto, e o arraste continua.
onDragSettleO overlay do arraste pousou (ou voltou ao lugar, ou foi pego pelo próximo arraste), com o evento que onDragEnd ou onDragCancel recebeu. Outros arrastes podem ter começado nesse meio-tempo.

Todo evento traz active, over, pointer, delta (o deslocamento desde o início), position (onde a caixa arrastada fica dentro de over, veja arrastar e soltar) e, para itens de uma lista ordenável, source. As formas completas estão na API de eventos.

onDragEnd é o lugar para o trabalho "depois do drop": salvar, limpar a seleção, analytics. O onDrop de uma lista roda antes da gravação dela, então ainda vê a ordem antiga.

Encontrar o alvo

Escolha uma função, depois arraste o alfinete ou o card sobre os alvos. Os selos mostram como a função os classifica neste quadro; o verde é o alvo.

Live

Carregando a 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">&lt;DnDProvider :collision="{{ mode }}"&gt;</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>

A cada quadro de um arraste, o provider passa para collision os alvos de drop em jogo, e ela retorna os ids deles, do melhor para o pior. Cinco funções vêm com a biblioteca:

FunçãoClassificaBoa para
pointerFirstO que está sob o ponteiro; quando não há nada, o que a caixa arrastada sobrepõe.O padrão: preciso sob o ponteiro, tolerante nos vãos.
pointerWithinSó o que está sob o ponteiro, o mais interno primeiro.Alvos precisos: um vão não solta em lugar nenhum.
rectIntersectionTodo alvo que a caixa arrastada sobrepõe: maior priority, depois o aninhamento mais profundo, depois a maior sobreposição.Itens grandes sobre alvos pequenos, quando o ponteiro está na borda do item.
closestCenterTodo alvo, pela distância do centro da caixa até o centro dele.Sempre alguém vence, mesmo longe: grades e listas de itens do mesmo tamanho.
closestCornersTodo alvo, pelas distâncias entre os quatro cantos.Colunas lado a lado (kanban), alvos com alturas muito diferentes.

Sob o ponteiro, o teste respeita o que o usuário vê: de alvos sobrepostos, só o de cima é atingido (front, nunca back onde eles se sobrepõem), e de alvos aninhados o mais interno vem primeiro (subfolder, depois folder).

Uma função sua

magnet.ts
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);
};
ArgumentoO que contém
pointerO ponteiro, depois dos modificadores.
overlayRectA caixa arrastada como é mostrada, null sem overlay.
candidatesTodo alvo em jogo: id, rect (a parte visível dele, recortada pelos contêineres de rolagem), priority, depth (0 no topo, maior quando aninhado).
hitsOs alvos sob o ponteiro, segundo o teste, do melhor para o pior.

Retorne um array vazio para "nenhum alvo". Alvos desativados, alvos dentro dos itens arrastados e alvos escondidos pelos seus contêineres de rolagem nunca chegam à função. Um alvo que recusa o arraste continua competindo: sobre ele, over.accepted é false e o drop é recusado (um alvo dentro de um item ordenável sai do caminho, para que a lista ordene sobre ele). A demo envolve a função escolhida para registrar a classificação: (args) => (ranked.value = detect(args)).

O provider amortece as trocas que a função pede. Um alvo que o arraste acabou de deixar não pode vencer de novo até o ponteiro se mover 10 px, então as fronteiras não piscam. Entre os alvos aninhados em um pai (os vãos de uma coluna de cards), o último alvo permanece.

Quando uma pressão vira arraste

Uma pressão não arrasta na hora. activation define, por tipo de ponteiro (mouse, touch, pen), o que ela espera: uma distance em px, um delay em ms (com uma tolerance além da qual a pressão desiste, para que a página possa rolar), ou o que vier primeiro dos dois.

<DnDProvider :activation="{ mouse: { distance: 8 }, touch: { delay: 300, tolerance: 8 } }">
PonteiroPadrão
mouse, pen{ distance: 4 }: um clique continua sendo um clique. Uma pressão do mouse em um handle começa na hora, e continua sendo um clique se não se mover.
touch{ delay: 200, tolerance: 5 }: um toque longo, para que as listas continuem rolando sob o dedo.

Um tipo que você omite mantém o seu padrão. A activation do próprio draggable vence a de uma área de arraste, que vence a do provider. Enquanto uma pressão espera, useDnD().status é 'activating' e activation.progress vai de 0 a 1; Escape ou useDnD().cancel() a cancela. Teste as restrições ao vivo, e desenhe um anel de toque longo a partir do progresso, em sensores.

Rolagem automática

Arraste um canal em direção ao topo ou ao fim da lista. As faixas verdes são as zonas de borda: quanto mais fundo em uma delas, mais rápido a lista rola.

Live

Carregando a 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>

Qualquer elemento com overflow: auto ou scroll rola sozinho, e a janela também. O provider rola primeiro o contêiner sob o ponteiro, depois os ancestrais dele, depois a janela.

OpçãoPadrãoO que define
threshold0.2A zona de borda, como fração do tamanho do contêiner, entre 24 e 96 px.
acceleration10A velocidade bem na borda, em px por quadro. Ela cresce com o quadrado da profundidade dentro da zona, então o começo é lento e preciso.
maxSpeed25A velocidade máxima. Além da borda (o ponteiro fora do contêiner), a velocidade continua crescendo até ela.

:auto-scroll="false" desliga a rolagem. Uma direção só rola depois que o ponteiro se moveu para aquele lado, então um item pego perto de uma borda não rola sozinho. Listas, árvores e áreas de arraste recebem um autoScroll próprio para o seu contêiner:

makeSortableList(list, { items: channels, autoScroll: { maxSpeed: 10 } });

Arrastes por teclado nunca rolam automaticamente: em vez disso, eles rolam o item até ficar visível. isAutoScrolling (do useDnD() ou de uma área de arraste) é true enquanto algo rola.

Vibração e a tecla de cópia

Um arraste por toque vibra rapidamente quando começa. :haptics="false" desliga isso; um objeto define cada vibração:

<DnDProvider :haptics="{ start: true, enter: 10, drop: 10, refuse: [20, 40, 20] }">
ChaveVibra quandoPadrão
startO toque longo vira arraste.true (10 ms)
enterO arraste entra em um alvo que o aceita.desligado
dropO drop é concluído.desligado
refuseO drop é recusado e os itens voltam ao lugar.desligado

Cada uma recebe true (o padrão), uma duração em ms, um padrão liga/desliga, ou false. Só os arrastes iniciados por toque vibram, e o Safari do iOS não consegue vibrar.

copyModifier é a tecla que, segurada durante um arraste entre listas, transforma o movimento em cópia: 'Alt' por padrão, qualquer KeyboardEvent.key, ou null para nenhuma. A lista de onde os itens vêm precisa de uma função clone; veja listas que compartilham itens.

Vários providers

A maioria dos apps precisa de exatamente um. Coloque a lista de desejos sob um provider próprio, e tente mover um livro de um lado para o outro: ele não consegue sair do seu provider, e o monitor externo deixa de ouvir a lista de desejos.

Live

Carregando a 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">&lt;DnDProvider&gt;</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>
  • Os providers são independentes: um arraste nunca passa de um para outro.
  • Providers aninhados são permitidos. Vence o dono mais próximo: um item pertence ao provider mais próximo acima dele, e o provider externo o ignora.
  • Cada provider tem a sua própria seleção, overlay, live region e monitores.

Um segundo provider só faz sentido para uma parte da página que precisa das suas próprias configurações de provider (colisão, teclas do teclado, anúncios) e cujos itens nunca saem dela. Se os itens não conseguem se mover entre duas partes da página, verifique se eles não estão sob dois providers.

Regras para uma parte da página (um quadro que encaixa na grade, um feed com um toque mais longo, um painel onde nada é arrastado) não precisam de um provider próprio:
makeDragArea(board, { modifiers: [snapToGrid(20)], activation: { touch: { delay: 400 } } });
Os itens dela continuam no provider do app e ainda podem ir para qualquer lugar. Veja restrições.

Um app montado em outro lugar, dentro de uma shadow root ou como um app Vue separado, pode se juntar a um provider existente: passe useDnDContext() para provideDnDContext(app, context) antes de montá-lo. O teste do que está sob o ponteiro e o overlay funcionam através de shadow roots.

SSR e Nuxt

O provider é seguro para SSR. Listas e itens são renderizados no servidor como qualquer outra marcação. O overlay e a live region são teleportados e montados só no cliente, então nunca causam um hydration mismatch. Os ids padrão vêm do useId() do Vue e batem entre servidor e cliente.

No Nuxt, o módulo registra o DnDProvider globalmente e importa os composables automaticamente: veja instalação.