Documentação/Guias

Arrastar e soltar

Arraste livre com makeDraggable e alvos de drop com makeDroppable — alvos tipados que aceitam alguns arrastes e recusam outros, uma planta baixa, lixeiras, pastas dentro de uma lista e itens simples que uma lista recebe.

Editar esta página

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

Use makeDraggable e makeDroppable para tudo o que não é uma lista ordenável: lixo nas lixeiras, móveis em uma planta baixa, arquivos em pastas, uma lixeira, um tabuleiro de xadrez.

Live

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

Um draggable

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>

Passe o id dos seus dados, como um getter. O id padrão vem do useId() e é novo a cada instância do componente, então não significa nada para o resto do seu app. As opções são lidas quando são necessárias, então um getter ou um ref as mantém atualizadas.

OpçãoO que faz
id, data, typeQuem é arrastado, o que ele carrega e o que o accept dos alvos compara.
handleUm seletor ou um elemento dentro dele: só ele inicia um arraste. Veja handles.
disabledNenhum arraste começa.
overlaytrue (padrão): uma cópia segue o ponteiro. Um componente: a sua própria prévia. false: o próprio elemento se move (abaixo).
axis, modifiersTravar o arraste em 'x' ou 'y', encaixá-lo, mantê-lo dentro de limites: veja restrições.
keyboardModeComo as setas o movem: por pixels (padrão) ou de alvo em alvo (abaixo).
activation, canDragQuando uma pressão vira arraste: veja sensores.
attributesrole, tabindex e aria-* para teclados e leitores de tela (padrão true).
RetornaO que é
isDraggingEste elemento está sendo arrastado.
selectedGravável, ligado à seleção do provider.
transformO deslocamento do arraste, para overlay: false.
idO id em uso.

Botões, inputs e outros filhos interativos não iniciam um arraste por ponteiro, então continuam funcionando dentro de um draggable. Todas as opções estão na API do makeDraggable.

Um alvo 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>
Retornatrue quando
isOverO arraste está sobre este alvo, aceitando ou não o arraste.
isAcceptingAlgo está sendo arrastado e este alvo o aceita: destaque os alvos válidos.
placementNão é um booleano: { top, right, bottom, left }, as metades do alvo em que o ponteiro está, ou null quando ele não está sobre o alvo. Use para "antes / depois deste card".

isOver sem isAccepting significa que o arraste está sobre um alvo que o recusa. Mostre isso, como as lixeiras acima fazem: um drop ali é cancelado e o item volta ao lugar. As lixeiras que não podem aceitar o item esmaecem enquanto ele viaja; mais visuais em estilização.

OpçãoO que faz
id, dataLidos de volta como over.id e over.data.
accept, acceptModeQuais arrastes ele aceita (próxima seção).
disabledNão aceita nada, e os arrastes passam por cima dele.
priorityO maior vence quando alvos se sobrepõem sem aninhamento (padrão 0).
onDragEnter, onDragLeaveO arraste entrou ou saiu.
onBeforeDrop, onDropDecidir o drop, depois aplicá-lo (abaixo).

Quais arrastes um alvo aceita

O type de um draggable é comparado com o accept de um alvo:

acceptAceita
omitido ou '*'Tudo.
'glass'Draggables do tipo glass, e os sem tipo.
['paper', 'cardboard']Qualquer um dos dois tipos, e os sem tipo.
({ active, items, types }) => booleanO que a sua função disser.

Um draggable sem type se encaixa em qualquer regra de string. type também pode ser um array (['paper', 'large']): basta um tipo combinar. Uma função decide com base em qualquer coisa do arraste. A lixeira de compostagem acima lê os dados dos itens, não o tipo deles:

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

accept é lido ao vivo, enquanto o arraste procura o seu alvo: passe um ref para mudá-lo.

Quando vários itens são arrastados de uma vez, acceptMode diz quantos precisam se encaixar: 'all' (padrão) ou 'some'. Com 'some', retorne do onBeforeDrop os ids que ficam, e os outros voltam ao lugar. Veja decidindo um drop.

O drop

O onDrop de um alvo de drop simples é onde você muda os seus dados. A biblioteca não move nada por conta própria:

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 do eventoO que contém
activeO arraste: id, ids, items (cada um { id, data, type, item }), data, type.
overEste alvo: id, data, element, rect, accepted.
pointer, deltaOnde o ponteiro está, e quanto ele se moveu desde o início.
positionOnde a caixa arrastada fica dentro deste alvo (próxima seção).
sourceDefinido quando os itens vêm de uma lista ordenável: { list, remove() }.

active.ids contém todos os ids arrastados, o pego primeiro. A ordem é onBeforeDrop (decidir), onDrop (aplicar), depois o drag-end do provider. Para ouvir todo arraste da página em um só lugar, use useDnDMonitor (veja o provider). Para perguntar antes de um drop, manter o arraste em andamento ou aceitar só alguns itens, veja decidindo um drop.

Deixar onde foi solto

Uma planta baixa, um canvas, um quadro: o item fica onde você o solta. Todo evento traz position, o left / top que coloca um item com posicionamento absoluto onde ele foi solto.

Live

Carregando a 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 é o canto superior esquerdo da caixa arrastada em over, medido a partir da borda do padding dele, com a rolagem somada: a borda do cômodo, as paredes, não conta. É null sobre nada.

Não calcule o lugar a partir de active.rect e delta: isso ignora a borda e a rolagem do alvo. Grades, limites e travas de eixo para esses quadros estão em restrições.

Sem overlay

Por padrão, uma cópia do elemento segue o ponteiro, e o elemento fica no lugar: os móveis no depósito funcionam assim. Com overlay: false não há cópia, e o próprio elemento se move pelo transform que o arraste retorna: os móveis no cômodo.

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 passa a ser o canto do próprio elemento, então o handler do drop continua o mesmo.
  • O elemento fica sob o ponteiro enquanto se move; ao procurar o alvo, a biblioteca olha através dele. Coloque-o acima dos vizinhos enquanto ele é arrastado (z-index).
  • Ele rola com a página, o overlay não: transform desconta o quanto o contêiner dele se moveu desde o início do arraste, então ele continua sob o ponteiro quando a página rola.
  • Ele se move dentro do seu próprio contêiner: um pai com overflow: hidden o recorta, e o contexto de empilhamento dele decide sobre o que ele passa. O overlay não tem nenhum dos dois problemas.

Para uma prévia personalizada em vez da cópia, veja o overlay.

Lixeiras

Um arraste que começa em uma lista ordenável traz e.source. Chame remove() e a lista descarta os itens:

Live

Carregando a 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 é o próprio item da lista. Guarde-o, e um desfazer o coloca de volta onde estava.
  • O onDrop da própria lista então roda com to.index === -1, e e.result contém a lista sem os itens.
  • O overlay encolhe dentro da zona, já que o item não aparece mais em lugar nenhum.
  • Uma lista também pode remover o que é solto fora de tudo: veja dropOutside.
Com o teclado, as setas movem o item de uma lista entre as posições da lista e das listas do seu grupo; depois do fim das listas, a próxima tecla o leva ao alvo de drop naquela direção, aqui a lixeira, e a seta oposta o traz de volta. Um botão, um menu ou a tecla Delete ainda é mais rápido para muita gente: ofereça um também.

Drop dentro de um item ordenável

Uma linha de uma lista ordenável também pode ser um alvo de drop: uma pasta que aceita favoritos, um grupo que aceita tarefas. Coloque um droppable no elemento do item:

Live

Carregando a 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
  },
});

Uma pasta aberta mostra os seus links como uma lista do mesmo grupo, que só aceita links: ali eles são ordenados, e podem ser arrastados de volta para o nível de cima ou para outra pasta.

Enquanto a lista ordenaria o arraste, um alvo assim só conta no meio do seu item (de 25 a 75% ao longo do eixo da lista). Ali o drop vai para dentro do item, e a lista não se mexe. Perto das bordas, a lista ordena como sempre.

  • Um alvo cujo accept recusa o arraste é pulado: a lista ordena sobre o item como sobre qualquer outro. As pastas só aceitam links, então uma pasta arrastada passa direto pelas outras.
  • Um alvo nunca recebe o seu próprio arraste.
  • Com o teclado, uma seta para primeiro na pasta; o próximo toque na mesma direção passa por ela.
  • O item não aparece mais depois do drop, então o overlay encolhe dentro da pasta em vez de voltar ao lugar.
  • Sobre a pasta, a lista mantém a posição da linha arrastada onde estava, para que nada se desloque sob o ponteiro. A posição recebe então data-dnd-placeholder="into": o drop não vai para lá, então esconda-a se quiser ([data-dnd-placeholder='into'] { opacity: 0 }).

O gerenciador de arquivos faz o mesmo em uma grade, com pastas aninhadas.

Draggables simples em uma lista

Uma lista ordenável recebe itens arrastados com makeDraggable quando sabe criar um item a partir deles:

Live

Carregando a 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}` }),
});

Enquanto o draggable está sobre a lista, o item criado fica no rascunho e é renderizado pelo seu template como qualquer outra linha: os horários da agenda já o contam. Ele se torna real no drop (mode: 'insert', from.list === null), e o overlay pousa nele.

  • Retorne undefined para recusar. Listas sem createItem ignoram draggables simples.
  • Dê ao novo item uma key própria: o bloco continua na paleta.
  • Com o teclado, uma seta em direção à lista traz o draggable para dentro, na altura dele, e as próximas setas o movem de posição em posição.

Arquivos do computador

Arquivos arrastados da área de trabalho ou de um gerenciador de arquivos não são arrastes da biblioteca: quem os carrega é o navegador. Um alvo de drop os aceita com external, e o drop os entrega em e.external:

Live

Carregando a 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 funciona como <input accept>: tipos MIME (image/*, application/pdf) e extensões (.md). true aceita qualquer arquivo, uma função decide arquivo por arquivo. links e text aceitam o que outros apps e abas arrastam, types qualquer outro tipo MIME.
  • O arraste roda como um arraste do provider: isOver, isAccepting, os eventos e os anúncios funcionam como em qualquer outro. Alvos sem external o ignoram. Não há overlay: o navegador desenha a imagem arrastada.
  • Enquanto os arquivos estão sobre a página, o navegador informa só os seus tipos (e.external.fileTypes). Os nomes, tamanhos e conteúdos chegam com o drop, e as extensões são verificadas nesse momento. Pastas ficam de fora; com multiple: false, um arraste de vários arquivos é recusado.

Listas também aceitam arquivos, links e texto, e abrem espaço para eles enquanto o arraste continua. Os itens também podem sair da página, para outro app ou outra janela do seu app: veja arquivos e outros apps.

Quem usa teclado e leitor de tela não consegue arrastar arquivos. Ofereça um <input type="file"> ao lado da zona, como a demo faz com Browse.

Teclado

Um draggable em foco é pego com Space ou Enter, solto com as mesmas teclas, e Escape cancela. Entre uma coisa e outra, as setas o movem:

keyboardModeUma seta
'pixels' (padrão)Move 25 px, o keyboard.step do provider. Com Shift, quatro vezes mais. Os móveis se movem assim.
'droppables'Pula para o alvo mais próximo naquela direção que o aceita. O lixo pula de lixeira em lixeira, e pula as lixeiras que o recusam.
makeDraggable(el, { id: () => props.waste.id, type: () => props.waste.type, keyboardMode: 'droppables' });

As teclas podem ser redefinidas no provider. Mais em acessibilidade.