Dokumentation/Leitfäden
Styling
Jeder Zustand eines Drags, gestylt – Data-Attribute für reines CSS, Refs für Klassen, Ziele, die Ja oder Nein sagen, das Overlay und die Seite während des Ziehens, die Standard-Styles, Tailwind und UI-Kits.
Diese Seite wurde von KI übersetzt und kann Ungenauigkeiten enthalten. Englisches Original lesen
Die Bibliothek stylt fast nichts. Jeder Zustand eines Drags erreicht dich auf zwei Wegen: als
data-dnd-*-Attribut für reines CSS und als Ref, das das Composable zurückgibt, für Klassen und
bedingtes Markup. Nimm, was zu deinem Stack passt, oder beides.
Die Data-Attribute
Die Zeilen unten haben überhaupt keine Zustandsklassen: Jedes Aussehen kommt von einem Attribut. Das Panel listet die Attribute auf der Seite, während sie kommen und gehen. Wähle zwei Zeilen mit Ctrl oder Meta aus und ziehe sie: Die übrigen Elemente eines Multi-Drags tauchen dann auch auf.
Demo wird geladen…
<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>| Attribut | Auf | Wann |
|---|---|---|
data-dnd-placeholder | einem sortierbaren Element | Es ist der Slot, in dem ein gezogenes Element landen wird. "into", solange der Drag über einem Drop-Ziel in einem Element ist: Der Drop geht dorthin, und der Slot hält nur seinen Platz. |
data-dnd-dragging | einem sortierbaren Element | Es wird gezogen, bleibt aber an seinem Platz: die übrigen Elemente eines Multi-Drags im Modus 'gaps'. |
data-dnd-dragging="pointer" oder "keyboard" | <html> | Während des ganzen Drags. |
data-dnd-overlay="dragging" oder "dropping" | der Overlay-Box | Immer. dropping, während sie landet und während ein asynchrones onBeforeDrop wartet. |
data-dnd-overlay-content | dem Inhalt des Overlays | Immer: das Element, das die Box ausfüllt und hält, was sie zeigt. Du animierst es; es bekommt das overlayClass des Providers. |
data-dnd-leaving | einer Kopie, die animateChanges hinterlässt | Während ein entferntes Element ausgeblendet wird. |
data-dnd-no-clone | von dir gesetzt | Lässt das Element in der Kopie des Overlays weg: einen Tooltip, ein Menü, ein Badge. |
/* 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>
- Die Kopie einer Zeile im Overlay behält ihre Klassen und Attribute, das Scope-Attribut von Vue
eingeschlossen, deshalb funktioniert
[data-dnd-overlay] .rowim eigenen<style scoped>der Zeile. Die Kopie hat keine eigenendata-dnd-*-Markierungen: Die Kopie eines Slots ist kein Slot. - Die Auswahl hat kein Attribut. Spiegle das Ref
selectedin eines (aria-selectedoben) oder nimm eine Klasse. - Einfache ziehbare Elemente und Drop-Ziele (
makeDraggable,makeDroppable) bekommen keinedata-dnd-*-Zustandsattribute: Style sie über ihre Refs.
Die zurückgegebenen Refs
Dieselben Zustände als Refs, plus die, die Listen und Ziele haben. Jede Zeile und jede Liste unten zeigt die Refs an, die gerade wahr sind, und ihre Klassen kommen aus ihnen. Die Icebox lehnt dringende Aufgaben ab: Ziehe eine darüber.
Demo wird geladen…
<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 | Zustand |
|---|---|
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: Dieses Element ist der Slot, in dem ein gezogenes Element landen wird. Bei einem Multi-Drag im Modus'group'sind auch die Slots der mitreisenden Elemente Platzhalter.isDragging: Das Element gehört zum Drag: jedes gezogene Element bei einem Multi-Drag.isAccepting: Etwas wird gezogen, und dieses Ziel nimmt es an. Lass die Stellen aufleuchten, an denen ein Drop möglich ist.isOver: Der Drag ist über diesem Ziel, ob es ihn annimmt oder nicht.isOver && !isAcceptingist eine Ablehnung: Zeig sie.indicator: wo die Drop-Linie im Indikator-Modus hinkommt.activation.progress: von 0 bis 1, solange ein langes Drücken per Touch oder eindelayläuft. Füll damit einen Ring.
Drop-Ziele: Ja und Nein
Ein Ziel hat während eines Drags vier Zustände: Es nimmt den Drag an; es nimmt ihn an, und der Zeiger ist darauf; es lehnt ihn ab; es lehnt ihn ab, und der Zeiger ist darauf. Zeig alle vier und sag es auch am gezogenen Element.
Demo wird geladen…
<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 */
Das gezogene Farbmuster ist der #overlay-Slot des Providers. Er bekommt over und accepted:
accepted ist false über einem Ziel, das den Drag ablehnt.
<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>
Außerhalb des Overlays sagt useDnD().over.value?.accepted dasselbe. Mehr zu
Overlay-Komponenten unter Das Overlay.
Das Overlay und die Seite
Zwei Attribute decken den Rest eines Drags ab: data-dnd-overlay auf der Overlay-Box und
data-dnd-dragging auf <html>, "pointer" oder "keyboard". Die Leiste über den Notizen liest
das zweite und sagt, welche Tasten funktionieren. Schalte das langsame Speichern ein: Das Overlay
bleibt dropping, wo du losgelassen hast, bis onBeforeDrop aufgelöst ist, und zeigt das an.
Demo wird geladen…
<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; }
droppingumfasst das Landen und das Warten auf ein asynchronesonBeforeDrop. Während es wartet, ist<html>nicht mehr im Drag-Modus: Der Cursor der Seite und die Textauswahl sind zurück, sodass ein Bestätigungsdialog wie gewohnt funktioniert.- Style, was in der Box ist. Die Box selbst bewegt die Bibliothek mit
transformund gibt ihr die Größe mitwidthundheight. Ihr Inhaltselement,[data-dnd-overlay-content], füllt sie aus und gehört dir: Hebe und neige dort das ganze Overlay (siehe Animationen). - Die Wurzel der Kopie hat inline
transform: none: Dreh oder vergrößere sie mitrotateundscale. Transitions und Keyframes laufen beide darauf: siehe Animationen. - Die Kopie zeigt das Element, wie es ist, und rät keinen Hintergrund. Eine Zeile, die die Liste
malt (ein
<li>in einem weißen<ul>, eine Tabellenzeile), braucht einen:[data-dnd-overlay] .row { background: Canvas; }. - Die Kopie sitzt in einem
<div>, das die Custom Properties trägt, die das Element geerbt hat (Theme-Farben,--tw-*), sie gelten im Overlay also weiter, und die eigenen Regeln der Kopie (in-data-[dnd-overlay]:ring-2) schlagen sie weiterhin. Wähle die Kopie über ihre Klasse aus, nicht über ihre Tiefe in der Box.
Standard-Styles
Der Provider fügt einmal pro Dokument ein paar Regeln ein. Sie stehen in :where(), deshalb ist
ihre Spezifität null, und jede deiner Regeln gewinnt:
: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;
}
| Regel | Warum |
|---|---|
| Der Slot bei 40 % | Damit eine Liste ganz ohne CSS richtig aussieht. Jede Demo oben überschreibt das. |
| Zurückgelassene Elemente bei 40 % | Dasselbe für einen Multi-Drag im Modus 'gaps'. |
Kein user-select | Ein Drag markiert nicht den Text, über den er fährt. |
Kein overflow-anchor | Die Seite scrollt nicht, um eine sich bewegende Zeile im Blick zu behalten. |
| Der Cursor des Drags | Während eines Drags per Zeiger, überall. grabbing, außer das gegriffene Element hat einen eigenen Cursor: Das ns-resize eines Resize-Handles, ein move, ein Bild behalten ihren für den ganzen Drag (grab, pointer und ähnliche werden trotzdem zu grabbing). cursor: grab auf ziehbaren Elementen ergänzt du selbst. Setze --dnd-cursor auf <html> für einen anderen Standard. |
| Kein Touch-Callout | iOS öffnet bei langem Drücken nicht das Link-Menü. |
:default-styles="false" auf dem Provider lässt sie weg. Die Bibliothek markiert <html>
trotzdem, hebt beim Start eines Drags weiterhin die Textauswahl auf und verhindert bis zu seinem Ende neue: Nur das
CSS ist weg. Die Regeln werden einmal eingefügt, vom ersten Provider, der sie will, und bleiben:
Schalte sie auf jedem Provider der Seite ab.
<DnDProvider :default-styles="false">
Tailwind CSS
Die Attribute funktionieren als Varianten: data-[…] für das Element selbst, eine beliebige
Variante für einen Vorfahren.
<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, die vor allen anderen deklariert ist. Die Utilities von Tailwind v4 und die darauf
aufbauenden UI-Kits liegen ebenfalls in Ebenen, und deren Ebenen kommen später, sodass sie ohne
! gewinnen. CSS ohne Ebene gewinnt sowieso.In Tailwind v4 ist in-data-[dnd-overlay=dragging]:shadow-xl eine kürzere Schreibweise für die
Vorfahren-Variante.
:global() den ganzen Selektor: :global(html[data-dnd-dragging]) .hint
wird zu html[data-dnd-dragging] allein kompiliert und blendet die Seite aus. Schreib den
Vorfahren ohne es, html[data-dnd-dragging] .hint: Vue scopet nur den letzten Teil.UI-Kits
- Die Composables nehmen neben einem Element auch eine Komponente:
useTemplateRefauf einer Karte oder einem Listenelement eines Kits funktioniert, und die Bibliothek liest sein Wurzelelement. - Gib Zustände an die eigenen Props des Kits weiter, wo es welche hat (
:active="selected",:color="isOver ? 'primary' : undefined"), und den Rest als Klassen. - Die Listenelemente eines Kits haben oft keinen eigenen Hintergrund: Die Liste malt ihn. Ihre
Kopie im Overlay ist dann durchsichtig. Gib ihr einen:
[data-dnd-overlay] .v-list-item { background: rgb(var(--v-theme-surface)); } - Die Kopie lässt die Ripple-Effekte beim Drücken von Vuetify, Quasar, PrimeVue und Naive UI weg.
Markiere alles andere, was nicht mitreisen soll, einen Tooltip oder einen Menü-Button, mit
data-dnd-no-clone.
Was du lassen solltest
- Setz kein CSS-
transitionauftransformodertranslatesortierbarer Elemente: Die Bibliothek animiert sie selbst und bricht solche Transitions ab, während sie misst. - Umschließe sortierbare Elemente nicht mit
<TransitionGroup>: Bewegungen würden doppelt animiert. NimmanimateChangesder Liste. - Animiere nicht
height,marginoderpaddingvon Zeilen in einer virtuellen Liste. - Schalte dagegen die Transitions, die du rund um Drags ergänzt, bei
prefers-reduced-motion: reduceab. Ihre eigenen schaltet die Bibliothek schon ab.