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.
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.
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
<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ção | O que faz |
|---|---|
id, data, type | Quem é arrastado, o que ele carrega e o que o accept dos alvos compara. |
handle | Um seletor ou um elemento dentro dele: só ele inicia um arraste. Veja handles. |
disabled | Nenhum arraste começa. |
overlay | true (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, modifiers | Travar o arraste em 'x' ou 'y', encaixá-lo, mantê-lo dentro de limites: veja restrições. |
keyboardMode | Como as setas o movem: por pixels (padrão) ou de alvo em alvo (abaixo). |
activation, canDrag | Quando uma pressão vira arraste: veja sensores. |
attributes | role, tabindex e aria-* para teclados e leitores de tela (padrão true). |
| Retorna | O que é |
|---|---|
isDragging | Este elemento está sendo arrastado. |
selected | Gravável, ligado à seleção do provider. |
transform | O deslocamento do arraste, para overlay: false. |
id | O 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
<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>
| Retorna | true quando |
|---|---|
isOver | O arraste está sobre este alvo, aceitando ou não o arraste. |
isAccepting | Algo está sendo arrastado e este alvo o aceita: destaque os alvos válidos. |
placement | Nã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ção | O que faz |
|---|---|
id, data | Lidos de volta como over.id e over.data. |
accept, acceptMode | Quais arrastes ele aceita (próxima seção). |
disabled | Não aceita nada, e os arrastes passam por cima dele. |
priority | O maior vence quando alvos se sobrepõem sem aninhamento (padrão 0). |
onDragEnter, onDragLeave | O arraste entrou ou saiu. |
onBeforeDrop, onDrop | Decidir o drop, depois aplicá-lo (abaixo). |
Quais arrastes um alvo aceita
O type de um draggable é comparado com o accept de um alvo:
accept | Aceita |
|---|---|
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 }) => boolean | O 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:
{
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:
<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 evento | O que contém |
|---|---|
active | O arraste: id, ids, items (cada um { id, data, type, item }), data, type. |
over | Este alvo: id, data, element, rect, accepted. |
pointer, delta | Onde o ponteiro está, e quanto ele se moveu desde o início. |
position | Onde a caixa arrastada fica dentro deste alvo (próxima seção). |
source | Definido 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.
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>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 é 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.
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.
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)` }),
}));
positionpassa 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:
transformdesconta 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: hiddeno 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:
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>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
onDropda própria lista então roda comto.index === -1, ee.resultconté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.
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:
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>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
acceptrecusa 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:
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>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
undefinedpara recusar. Listas semcreateItemignoram 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:
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>const { isOver, isAccepting } = makeDroppable(el, {
external: { files: 'image/*', multiple: false },
onDrop: (e) => show(e.external!.files[0]!),
});
filesfunciona como<input accept>: tipos MIME (image/*,application/pdf) e extensões (.md).trueaceita qualquer arquivo, uma função decide arquivo por arquivo.linksetextaceitam o que outros apps e abas arrastam,typesqualquer 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 semexternalo 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; commultiple: 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.
<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:
keyboardMode | Uma 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.