Documentação/Guias
Estilização
Cada estado de um arraste, estilizado — atributos data para CSS puro, refs para classes, alvos que dizem sim ou não, o overlay e a página durante o arraste, os estilos padrão, Tailwind e UI kits.
Esta página foi traduzida por IA e pode conter imprecisões. Ler o original em inglês
A biblioteca estiliza quase nada. Cada estado de um arraste chega até você de duas formas: como
um atributo data-dnd-*, para CSS puro, e como um ref que o composable retorna, para classes e
marcação condicional. Use o que combinar com a sua stack, ou os dois.
Os atributos data
As linhas abaixo não têm nenhuma classe de estado: todo visual vem de um atributo. O painel lista os atributos da página conforme eles aparecem e somem. Selecione duas linhas com Ctrl ou Meta e arraste-as: os outros de um multi-drag também aparecem.
Carregando a demo…
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Inspector from './Inspector.vue';
import TaskList from './TaskList.vue';
const demo = useTemplateRef<HTMLElement>('demo');
</script>
<template>
<DnDProvider>
<div ref="demo" class="demo">
<TaskList />
<Inspector :scope="demo" />
</div>
</DnDProvider>
</template>
<style scoped>
.demo {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 20px;
}
</style>| Atributo | Em | Quando |
|---|---|---|
data-dnd-placeholder | um item ordenável | Ele é a posição em que um item arrastado vai pousar. "into" enquanto o arraste está sobre um alvo de drop dentro de um item: o drop vai para lá, e a posição só guarda o seu lugar. |
data-dnd-dragging | um item ordenável | Ele é arrastado, mas fica no lugar: os outros itens de um multi-drag no modo 'gaps'. |
data-dnd-dragging="pointer" ou "keyboard" | <html> | Durante todo o arraste. |
data-dnd-overlay="dragging" ou "dropping" | a caixa do overlay | Sempre. dropping enquanto ela pousa, e enquanto um onBeforeDrop assíncrono espera. |
data-dnd-overlay-content | o conteúdo do overlay | Sempre: o elemento que preenche a caixa e contém o que ela mostra. Animá-lo fica por sua conta; ele recebe o overlayClass do provider. |
data-dnd-leaving | uma cópia deixada por animateChanges | Enquanto um item que saiu desaparece aos poucos. |
data-dnd-no-clone | definido por você | Deixa o elemento fora da cópia do overlay: um tooltip, um menu, um selo. |
/* The slot a dragged row will land in: a striped gap. */
.row[data-dnd-placeholder] {
opacity: 1;
background: repeating-linear-gradient(-45deg, color-mix(in srgb, #42b883 16%, transparent) 0 6px, transparent 6px 12px);
}
.row[data-dnd-placeholder] > * {
visibility: hidden; /* an empty slot of the row's exact size */
}
/* Selected rows left in place by a multi-drag (`multiple: 'gaps'`). */
.row[data-dnd-dragging] {
opacity: 0.4;
}
/* The copy in the overlay, while it travels. */
[data-dnd-overlay='dragging'] .row {
box-shadow: 0 0 0 1.5px #42b883, 0 14px 24px -10px rgb(0 0 0 / 0.55);
}
<li ref="el" class="row" :aria-selected="selected">
<span class="title">{{ task.title }}</span>
<button type="button" data-dnd-no-clone @click.stop="task.done = !task.done">✓</button>
</li>
- A cópia de uma linha no overlay mantém as classes e os atributos dela, incluindo o atributo de
escopo do Vue, então
[data-dnd-overlay] .rowfunciona no próprio<style scoped>da linha. A cópia não tem marcasdata-dnd-*próprias: uma cópia da posição não é uma posição. - A seleção não tem atributo. Espelhe o ref
selectedem um (aria-selectedacima), ou use uma classe. - Draggables simples e alvos de drop (
makeDraggable,makeDroppable) não recebem atributos de estadodata-dnd-*: estilize-os com os refs deles.
Os refs retornados
Os mesmos estados como refs, mais os que as listas e os alvos têm. Cada linha e cada lista abaixo
mostra os refs que estão true agora, e as classes delas vêm deles. O Icebox recusa tarefas
urgentes: arraste uma sobre ele.
Carregando a demo…
<script setup lang="ts">
import { reactive } from 'vue';
import { DnDProvider, type SortablePutContext } from '@vue-dnd-kit/core';
import RefList from './RefList.vue';
const lists = reactive([
{
id: 'sprint',
title: 'Sprint',
group: { name: 'work' },
tasks: [
{ id: 'login', title: 'Fix the login', urgent: true },
{ id: 'search', title: 'Search filters' },
{ id: 'avatars', title: 'Upload avatars' },
{ id: 'invoices', title: 'PDF invoices' },
],
},
{
id: 'icebox',
title: 'Icebox',
// Urgent tasks can't go on ice.
group: { name: 'work', put: ({ types }: SortablePutContext) => !types.includes('urgent') },
tasks: [{ id: 'themes', title: 'Custom themes' }],
},
]);
</script>
<template>
<DnDProvider>
<div class="lists">
<RefList v-for="list in lists" :key="list.id" :list="list" />
</div>
</DnDProvider>
</template>
<style scoped>
.lists {
display: flex;
justify-content: center;
gap: 12px;
}
</style><script setup lang="ts">
const { isDragging, isPlaceholder, selected } = makeSortableItem(el, { item: () => props.task });
</script>
<template>
<li ref="el" class="row" :class="{ placeholder: isPlaceholder, dragging: isDragging, selected }">…</li>
</template>
<style scoped>
.row.selected { box-shadow: 0 0 0 1.5px #3b82f6; }
.row.placeholder { opacity: 1; box-shadow: inset 0 0 0 1.5px #42b883; } /* beats the default 40% */
.row.placeholder .title { visibility: hidden; }
</style>
<script setup lang="ts">
const { items, isOver, isAccepting } = makeSortableList(el, { items, group });
</script>
<template>
<section :class="{ accepting: isAccepting, over: isOver && isAccepting, refused: isOver && !isAccepting }">…</section>
</template>
| Composable | Estado |
|---|---|
makeSortableItem | isDragging, isPlaceholder, selected, indicator, index |
makeSortableList, makeSortableTree | isOver, isAccepting, indicator, activeKey, activeIndex |
makeDraggable | isDragging, selected, transform |
makeDroppable | isOver, isAccepting, placement |
makeDragArea | isDragging, isAutoScrolling |
useDnD() | status, active, over, pointer, activation.progress, isAutoScrolling |
isPlaceholder: este elemento é a posição em que um item arrastado vai pousar. Em um multi-drag no modo'group', as posições dos itens que viajam junto também são placeholders.isDragging: o item faz parte do arraste: todos os itens arrastados em um multi-drag.isAccepting: algo está sendo arrastado, e este alvo o aceita. Destaque os lugares onde um drop é possível.isOver: o arraste está sobre este alvo, aceitando-o ou não.isOver && !isAcceptingé uma recusa: mostre-a.indicator: onde fica a linha de drop no modo indicador.activation.progress: de 0 a 1 enquanto corre um toque longo ou umdelay. Preencha um anel com ele.
Alvos de drop: sim e não
Um alvo tem quatro visuais durante um arraste: ele aceita o arraste; aceita e o ponteiro está sobre ele; recusa; recusa e o ponteiro está sobre ele. Mostre os quatro, e diga isso também no item arrastado.
Carregando a demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Palette from './Palette.vue';
import Swatch from './Swatch.vue';
const SWATCHES = [
{ id: 'orange', color: '#f97316', tone: 'warm' as const },
{ id: 'blue', color: '#3b82f6', tone: 'cool' as const },
{ id: 'stone', color: '#a8a29e', tone: 'neutral' as const },
{ id: 'red', color: '#ef4444', tone: 'warm' as const },
{ id: 'cyan', color: '#06b6d4', tone: 'cool' as const },
{ id: 'slate', color: '#475569', tone: 'neutral' as const },
{ id: 'amber', color: '#eab308', tone: 'warm' as const },
{ id: 'violet', color: '#8b5cf6', tone: 'cool' as const },
];
</script>
<template>
<DnDProvider>
<div class="studio">
<div class="swatches">
<Swatch v-for="swatch in SWATCHES" :key="swatch.id" :swatch="swatch" />
</div>
<div class="palettes">
<Palette tone="warm" />
<Palette tone="cool" />
<Palette tone="neutral" />
</div>
</div>
<!-- The dragged swatch says it too: over a palette, `accepted` tells yes from no. -->
<template #overlay="{ active, over, accepted }">
<div class="lifted" :class="{ yes: over && accepted, no: over && !accepted }" :style="{ background: (active.data as { color: string }).color }">
<span v-if="over" class="badge">{{ accepted ? '+' : '✕' }}</span>
</div>
</template>
</DnDProvider>
</template>
<style scoped>
.studio {
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
}
.swatches {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
}
.palettes {
display: flex;
justify-content: center;
gap: 10px;
width: 100%;
}
/* Fills the overlay box, which has the swatch's size. */
.lifted {
position: relative;
width: 100%;
height: 100%;
border-radius: 10px;
box-shadow: 0 12px 22px -8px rgb(0 0 0 / 0.55);
transition:
filter 0.15s,
scale 0.15s;
}
.lifted.yes {
scale: 1.12;
}
.lifted.no {
filter: grayscale(1) brightness(0.7);
}
.badge {
position: absolute;
top: -7px;
right: -7px;
display: grid;
place-items: center;
width: 18px;
height: 18px;
border-radius: 50%;
background: #42b883;
color: #fff;
font: 700 12px/1 system-ui, sans-serif;
}
.no .badge {
background: #f43f5e;
font-size: 9px;
}
</style>const { isOver, isAccepting } = makeDroppable(el, { accept: props.tone, onDrop });
const { active } = useDnD();
const state = computed(() => {
if (!active.value) return '';
if (isOver.value) return isAccepting.value ? 'over' : 'refused';
return isAccepting.value ? 'accepting' : 'refusing';
});
.palette.accepting { border-style: dashed; border-color: #42b883; } /* an invitation */
.palette.over { border-color: #42b883; background: color-mix(in srgb, #42b883 14%, Canvas); }
.palette.refusing { opacity: 0.45; } /* out of the way */
.palette.refused { border-color: #f43f5e; animation: shake 0.3s; } /* a drop here is cancelled */
A amostra arrastada é o slot #overlay do provider. Ele recebe over e accepted: accepted é
false sobre um alvo que recusa o arraste.
<template #overlay="{ active, over, accepted }">
<div
class="lifted"
:class="{ yes: over && accepted, no: over && !accepted }"
:style="{ background: (active.data as { color: string }).color }"
>
<span v-if="over" class="badge">{{ accepted ? '+' : '✕' }}</span>
</div>
</template>
Fora do overlay, useDnD().over.value?.accepted diz o mesmo. Mais sobre componentes de overlay em
o overlay.
O overlay e a página
Dois atributos cobrem o resto de um arraste: data-dnd-overlay na caixa do overlay, e
data-dnd-dragging no <html>, "pointer" ou "keyboard". A barra acima das notas lê o segundo
e diz quais teclas funcionam. Ligue o salvamento lento: o overlay fica em dropping onde você
soltou, até o onBeforeDrop resolver, e mostra isso.
Carregando a demo…
<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import NoteBoard from './NoteBoard.vue';
const slowSave = ref(false);
</script>
<template>
<DnDProvider>
<div class="desk">
<!-- `<html data-dnd-dragging="pointer | keyboard">` for the whole drag: the bar reads it in CSS. -->
<p class="bar">
<span class="idle">Drag a note to reorder</span>
<span class="pointer">Release to drop · Esc cancels</span>
<span class="keyboard">Arrows move · Space drops · Esc cancels</span>
</p>
<NoteBoard :slow-save="slowSave" />
<label class="switch">
<input v-model="slowSave" type="checkbox" />
Save on drop (1 s)
</label>
</div>
</DnDProvider>
</template>
<style scoped>
.desk {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.bar {
margin: 0;
padding: 7px 14px;
border-radius: 99px;
background: color-mix(in srgb, currentColor 7%, transparent);
font: 12px/1 ui-monospace, Menlo, Consolas, monospace;
transition:
background 0.2s,
color 0.2s;
}
.bar .pointer,
.bar .keyboard,
html[data-dnd-dragging] .bar .idle {
display: none;
}
html[data-dnd-dragging] .bar {
background: #42b883;
color: #fff;
}
html[data-dnd-dragging='pointer'] .bar .pointer,
html[data-dnd-dragging='keyboard'] .bar .keyboard {
display: inline;
}
.switch {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
cursor: pointer;
}
.switch input {
accent-color: #42b883;
}
</style>.bar .pointer,
.bar .keyboard,
html[data-dnd-dragging] .bar .idle { display: none; }
html[data-dnd-dragging='pointer'] .bar .pointer,
html[data-dnd-dragging='keyboard'] .bar .keyboard { display: inline; }
[data-dnd-overlay='dragging'] .note {
box-shadow: 0 0 0 2px #42b883, 0 22px 30px -14px rgb(0 0 0 / 0.6);
}
/* Landing, and waiting for the save. */
[data-dnd-overlay='dropping'] .note { opacity: 0.85; }
[data-dnd-overlay='dropping'] .saving { display: flex; }
droppingcobre o pouso e a espera de umonBeforeDropassíncrono. Enquanto ele espera, o<html>sai do modo de arraste: o cursor e a seleção de texto da página voltam, então um diálogo de confirmação funciona como sempre.- Estilize o que está dentro da caixa. A biblioteca move a própria caixa com
transforme a dimensiona comwidtheheight. O elemento de conteúdo dela,[data-dnd-overlay-content], a preenche e é seu: erga e incline o overlay inteiro ali (veja animações). - A raiz da cópia tem
transform: noneinline: gire-a ou aumente-a comrotateescale. Transições e keyframes rodam nela: veja animações. - A cópia mostra o elemento como ele é e não adivinha fundo nenhum. Uma linha que a lista pinta
(um
<li>em um<ul>branco, uma linha de tabela) precisa de um:[data-dnd-overlay] .row { background: Canvas; }. - A cópia fica em uma
<div>que carrega as custom properties que o elemento herdava (cores do tema,--tw-*), então elas continuam valendo no overlay, e as regras da própria cópia (in-data-[dnd-overlay]:ring-2) ainda vencem. Selecione a cópia pela classe, não pela profundidade dela na caixa.
Estilos padrão
O provider injeta algumas regras uma vez por documento. Elas ficam dentro de :where(), então a
especificidade delas é zero e qualquer regra sua vence:
:where([data-dnd-placeholder]) { opacity: 0.4; }
:where([data-dnd-dragging]:not(html)) { opacity: 0.4; }
:where(html[data-dnd-dragging]), :where(html[data-dnd-dragging] *) {
user-select: none !important;
overflow-anchor: none !important;
}
:where(html[data-dnd-dragging='pointer']), :where(html[data-dnd-dragging='pointer'] *) {
cursor: var(--dnd-cursor, grabbing) !important;
-webkit-touch-callout: none !important;
}
| Regra | Por quê |
|---|---|
| A posição a 40% | Para que uma lista fique certa sem CSS nenhum. Todas as demos acima a sobrescrevem. |
| Itens deixados para trás a 40% | O mesmo, para um multi-drag no modo 'gaps'. |
Sem user-select | Um arraste não seleciona o texto pelo qual passa. |
Sem overflow-anchor | A página não rola para manter uma linha em movimento à vista. |
| O cursor do arraste | Durante um arraste por ponteiro, em todo lugar. grabbing, a menos que o elemento pego tenha um cursor próprio: o ns-resize de uma alça de redimensionar, um move, uma imagem mantêm o seu durante todo o arraste (grab, pointer e afins ainda viram grabbing). cursor: grab nos itens arrastáveis fica por sua conta. Defina --dnd-cursor no <html> para outro padrão. |
| Sem touch callout | O iOS não abre o menu de link sob um toque longo. |
:default-styles="false" no provider as deixa de fora. A biblioteca ainda marca o <html>, e
ainda limpa a seleção quando um arraste começa e impede novas seleções até ele terminar: só o CSS some. As regras
são injetadas uma vez, pelo primeiro provider que as quer, e ficam: desligue-as em todos os
providers da página.
<DnDProvider :default-styles="false">
Tailwind CSS
Os atributos funcionam como variantes: data-[…] para o próprio elemento, uma variante
arbitrária para um ancestral.
<li
ref="el"
class="rounded-lg bg-white shadow-sm
data-[dnd-placeholder]:bg-transparent data-[dnd-placeholder]:opacity-100
data-[dnd-placeholder]:outline-dashed
[[data-dnd-overlay=dragging]_&]:shadow-xl"
:class="{ 'ring-2 ring-emerald-500': selected }"
>
<p class="hidden [html[data-dnd-dragging=keyboard]_&]:block">Arrows move · Space drops</p>
vue-dnd-kit,
declarada antes de qualquer outra. Os utilitários do Tailwind v4 e os UI kits feitos com ele
também vivem em layers, e as deles vêm depois, então vencem sem !. CSS fora de layers vence de
qualquer jeito.No Tailwind v4, in-data-[dnd-overlay=dragging]:shadow-xl é um jeito mais curto de escrever a
variante de ancestral.
:global() pega o seletor inteiro: :global(html[data-dnd-dragging]) .hint
compila só para html[data-dnd-dragging], e esconde a página. Escreva o ancestral sem ele,
html[data-dnd-dragging] .hint: o Vue aplica o escopo só à última parte.UI kits
- Os composables aceitam um componente assim como um elemento:
useTemplateRefno card ou no item de lista de um kit funciona, e a biblioteca lê o elemento raiz dele. - Passe o estado para as props do próprio kit quando ele as tiver (
:active="selected",:color="isOver ? 'primary' : undefined"), e o resto como classes. - Os itens de lista de um kit muitas vezes não têm fundo próprio: a lista o pinta. A cópia deles
no overlay fica então transparente. Dê um fundo a ela:
[data-dnd-overlay] .v-list-item { background: rgb(var(--v-theme-surface)); } - A cópia deixa de fora os ripples de pressão do Vuetify, Quasar, PrimeVue e Naive UI. Marque
qualquer outra coisa que não deve viajar, um tooltip ou um botão de menu, com
data-dnd-no-clone.
O que não fazer
- Não coloque um
transitionCSS emtransformoutranslatede itens ordenáveis: a biblioteca os anima ela mesma, e cancela essas transições enquanto mede. - Não envolva itens ordenáveis em
<TransitionGroup>: os movimentos seriam animados duas vezes. Use oanimateChangesda lista. - Não anime
height,marginoupaddingde linhas em uma lista virtual. - Desligue as transições que você adiciona em volta dos arrastes sob
prefers-reduced-motion: reduce. A biblioteca já desliga as dela.