Documentación/Guías
Estilos
Cada estado de un arrastre, con estilo — atributos data para CSS simple, refs para clases, destinos que dicen sí o no, el overlay y la página mientras arrastras, los estilos por defecto, Tailwind y kits de UI.
Esta página la ha traducido una IA y puede contener imprecisiones. Leer el original en inglés
La biblioteca casi no da estilo a nada. Cada estado de un arrastre te llega de dos formas: como un
atributo data-dnd-*, para CSS simple, y como una ref que devuelve el composable, para clases y
marcado condicional. Usa la que encaje con tu stack, o ambas.
Los atributos data
Las filas de abajo no tienen ninguna clase de estado: cada aspecto sale de un atributo. El panel muestra los atributos de la página a medida que aparecen y desaparecen. Selecciona dos filas con Ctrl o Meta y arrástralas: también aparecen los demás de un multi-drag.
Cargando la 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 | En | Cuándo |
|---|---|---|
data-dnd-placeholder | un ítem ordenable | Es el hueco donde aterrizará un ítem arrastrado. "into" mientras el arrastre está sobre un destino de drop dentro de un ítem: el drop va ahí, y el hueco solo guarda su sitio. |
data-dnd-dragging | un ítem ordenable | Se está arrastrando pero se queda en su sitio: los demás ítems de un multi-drag en modo 'gaps'. |
data-dnd-dragging="pointer" o "keyboard" | <html> | Durante todo el arrastre. |
data-dnd-overlay="dragging" o "dropping" | la caja del overlay | Siempre. dropping mientras aterriza, y mientras espera un onBeforeDrop asíncrono. |
data-dnd-overlay-content | el contenido del overlay | Siempre: el elemento que llena la caja y contiene lo que muestra. Animarlo te toca a ti; recibe el overlayClass del provider. |
data-dnd-leaving | una copia dejada por animateChanges | Mientras se desvanece un ítem que se fue. |
data-dnd-no-clone | lo pones tú | Deja el elemento fuera de la copia del overlay: un tooltip, un menú, una insignia. |
/* 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>
- La copia de una fila en el overlay conserva sus clases y atributos, incluido el atributo de
scope de Vue, así que
[data-dnd-overlay] .rowfunciona en el propio<style scoped>de la fila. La copia no tiene marcasdata-dnd-*propias: una copia del hueco no es un hueco. - La selección no tiene atributo. Refleja la ref
selecteden uno (aria-selectedarriba), o usa una clase. - Los elementos arrastrables simples y los destinos de drop (
makeDraggable,makeDroppable) no reciben atributos de estadodata-dnd-*: dales estilo con sus refs.
Las refs devueltas
Los mismos estados como refs, más los que tienen las listas y los destinos. Cada fila y cada lista de abajo muestra las refs que son verdaderas en este momento, y sus clases salen de ellas. Icebox rechaza las tareas urgentes: arrastra una sobre ella.
Cargando la 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 es el hueco donde aterrizará un ítem arrastrado. En un multi-drag en modo'group', los huecos de los ítems que viajan con él también son placeholders.isDragging: el ítem forma parte del arrastre; cada ítem arrastrado en un multi-drag.isAccepting: se está arrastrando algo y este destino lo acepta. Resalta los sitios donde es posible un drop.isOver: el arrastre está sobre este destino, lo acepte o no.isOver && !isAcceptinges un rechazo: muéstralo.indicator: dónde va la línea del drop en modo indicador.activation.progress: de 0 a 1 mientras dura una pulsación larga táctil o undelay. Llena un anillo con él.
Destinos de drop: sí y no
Un destino tiene cuatro aspectos mientras dura un arrastre: acepta el arrastre, lo acepta y el puntero está encima, lo rechaza, lo rechaza y el puntero está encima. Muestra los cuatro, y dilo también en el ítem arrastrado.
Cargando la 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 */
La muestra arrastrada es el slot #overlay del provider. Recibe over y accepted: accepted
es false sobre un destino que rechaza el arrastre.
<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>
Fuera del overlay, useDnD().over.value?.accepted dice lo mismo. Más sobre los componentes de
overlay en el overlay.
El overlay y la página
Dos atributos cubren el resto de un arrastre: data-dnd-overlay en la caja del overlay, y
data-dnd-dragging en <html>, "pointer" o "keyboard". La barra que hay encima de las notas
lee el segundo y dice qué teclas funcionan. Activa el guardado lento: el overlay se queda en
dropping donde soltaste, hasta que se resuelve onBeforeDrop, y lo muestra.
Cargando la 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; }
droppingcubre el aterrizaje y la espera de unonBeforeDropasíncrono. Mientras espera,<html>sale del modo de arrastre: vuelven el cursor y la selección de texto de la página, así que un diálogo de confirmación funciona como siempre.- Da estilo a lo que hay dentro de la caja. La biblioteca mueve la propia caja con
transformy le da tamaño conwidthyheight. Su elemento de contenido,[data-dnd-overlay-content], la llena y es tuyo: levanta e inclina ahí el overlay entero (consulta animaciones). - La raíz de la copia tiene
transform: noneen línea: gírala o agrándala conrotateyscale. En ella funcionan tanto las transiciones como los keyframes: consulta animaciones. - La copia muestra el elemento tal como es y no adivina ningún fondo. Una fila que pinta la lista
(un
<li>en un<ul>blanco, una fila de tabla) necesita uno:[data-dnd-overlay] .row { background: Canvas; }. - La copia va dentro de un
<div>que lleva las propiedades personalizadas que el elemento heredaba (colores del tema,--tw-*), así que siguen aplicándose en el overlay, y las reglas propias de la copia (in-data-[dnd-overlay]:ring-2) siguen ganándoles. Selecciona la copia por su clase, no por su profundidad dentro de la caja.
Estilos por defecto
El provider inyecta unas pocas reglas, una vez por documento. Van envueltas en :where(), así que
su especificidad es cero y cualquier regla tuya gana:
: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;
}
| Regla | Por qué |
|---|---|
| El hueco al 40 % | Para que una lista se vea bien sin nada de CSS. Todas las demos de arriba la sobrescriben. |
| Los ítems que se quedan atrás al 40 % | Lo mismo, para un multi-drag en modo 'gaps'. |
Sin user-select | Un arrastre no selecciona el texto por el que pasa. |
Sin overflow-anchor | La página no se desplaza para mantener a la vista una fila en movimiento. |
| El cursor del arrastre | Durante un arrastre de puntero, en todas partes. grabbing, a menos que el elemento agarrado tenga un cursor propio: el ns-resize de un asa de redimensionar, un move o una imagen conservan el suyo durante todo el arrastre (grab, pointer y similares se siguen convirtiendo en grabbing). cursor: grab en los ítems arrastrables lo añades tú. Pon --dnd-cursor en <html> para otro cursor por defecto. |
| Sin menú contextual táctil | iOS no abre el menú de enlaces bajo una pulsación larga. |
:default-styles="false" en el provider las omite. La biblioteca sigue marcando <html>, y sigue
borrando la selección cuando empieza un arrastre e impidiendo nuevas hasta que termina: solo desaparece el CSS. Las
reglas se inyectan una vez, el primer provider que las quiere, y se quedan: desactívalas en todos
los providers de la página.
<DnDProvider :default-styles="false">
Tailwind CSS
Los atributos funcionan como variantes: data-[…] para el propio elemento, una variante
arbitraria para un ancestro.
<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 que ninguna otra. Las utilidades de Tailwind v4 y los kits de UI
construidos sobre él también viven en layers, que van después, así que ganan sin !. El CSS sin
layer gana de todos modos.En Tailwind v4, in-data-[dnd-overlay=dragging]:shadow-xl es una forma más corta de escribir la
variante de ancestro.
:global() se lleva el selector entero: :global(html[data-dnd-dragging]) .hint
se compila como html[data-dnd-dragging] a secas, y oculta la página. Escribe el ancestro sin él,
html[data-dnd-dragging] .hint: Vue solo aplica el scope a la última parte.Kits de UI
- Los composables aceptan tanto un componente como un elemento:
useTemplateRefsobre una tarjeta o un ítem de lista de un kit funciona, y la biblioteca lee su elemento raíz. - Pasa el estado a las props propias del kit cuando las tenga (
:active="selected",:color="isOver ? 'primary' : undefined"), y el resto como clases. - Los ítems de lista de un kit a menudo no tienen fondo propio: lo pinta la lista. Su copia en el
overlay es entonces transparente. Dale uno:
[data-dnd-overlay] .v-list-item { background: rgb(var(--v-theme-surface)); } - La copia deja fuera los ripples de pulsación de Vuetify, Quasar, PrimeVue y Naive UI. Marca
cualquier otra cosa que no deba viajar, un tooltip o un botón de menú, con
data-dnd-no-clone.
Qué no hacer
- No pongas una
transitionCSS en eltransformo eltranslatede los ítems ordenables: la biblioteca los anima ella misma, y cancela esas transiciones mientras mide. - No envuelvas los ítems ordenables en
<TransitionGroup>: los movimientos se animarían dos veces. Usa elanimateChangesde la lista. - No animes
height,marginnipaddingde las filas de una lista virtual. - Eso sí, detén bajo
prefers-reduced-motion: reducelas transiciones que añadas alrededor de los arrastres. La biblioteca ya detiene las suyas.