Dokumentation/Leitfäden
Animationen
Elemente gleiten beim Sortieren, der Drop landet auf seinem Slot, und auch Datenänderungen können animiert werden. Stimme jede davon ab, animiere Overlay, Slot und Listen mit CSS oder bring motion oder GSAP mit.
Diese Seite wurde von KI übersetzt und kann Ungenauigkeiten enthalten. Englisches Original lesen
Drei Dinge animieren von selbst: die Elemente, die beim Sortieren gleiten, das Overlay, das beim Drop landet, und, wenn du es einschaltest, die Änderungen deiner Daten, die kein Drag gemacht hat. Jedes davon nimmt dieselben Werte, am Provider oder näher an dem, was es animiert:
| Option | Wo | Standard | Was sie animiert |
|---|---|---|---|
animation | Provider, Liste, Baum | an, 200 ms | Elemente, die beim Sortieren, beim Drop und beim Abbrechen an ihre neuen Plätze gleiten. |
dropAnimation | Provider, ziehbares Element, sortierbares Element, Liste, Baum (overDropAnimation an einem Drop-Ziel) | an, 250 ms | Das Overlay, das auf seinem Slot landet oder beim Abbrechen zurückfliegt. |
animateChanges | Provider, Liste, Baum | aus | Änderungen deiner Daten, die kein Drag gemacht hat: hinzugefügte, entfernte, verschobene oder herausgefilterte Elemente, ein- oder ausgeblendete Baumzeilen. |
Jede nimmt true (das Standard-Timing), false (aus), { duration, easing } oder eine
Funktion, die sie selbst animiert:
<DnDProvider
:animation="{ duration: 180, easing: 'cubic-bezier(.2, .8, .2, 1)' }"
:drop-animation="{ duration: 300 }"
animate-changes
>
Die eigenen animation und animateChanges einer Liste oder eines Baums gewinnen gegen die des
Providers; zu dropAnimation siehe Pro Element und pro Ziel. Der Rest
der Bewegung gehört dir: Das Overlay, der Slot und die Listen sind Elemente mit
Data-Attributen und Refs, und CSS oder eine Animationsbibliothek bewegt
sie.
Sortieren
Während du ziehst, ordnet sich der Entwurf der Liste neu, und die Elemente gleiten an ihre neuen
Plätze. Die Hand unten setzt beide Optionen an ihrer Liste: Wähle eine animation und sortiere
die Karten, wähle eine dropAnimation und lass eine los.
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Hand from './Hand.vue';
</script>
<template>
<DnDProvider>
<Hand />
</DnDProvider>
</template>const springy: LayoutAnimation = ({ element, from, to, reducedMotion }) => {
if (reducedMotion) return;
return element.animate(
[{ translate: `${from.left - to.left}px ${from.top - to.top}px` }, { translate: '0 0' }],
{ duration: 500, easing: 'cubic-bezier(.34, 1.56, .64, 1)' },
);
};
const animation = shallowRef<LayoutAnimation>(true); // true, { duration: 600 }, springy or false
const dropAnimation = shallowRef<DropAnimation>(true); // true, { duration: 700 }, toss or false
const { items } = makeSortableList(table, {
items: cards,
strategy: 'horizontal',
dropOutside: 'cancel', // let go off the table: the card flies back
animation,
dropAnimation,
});
Es ist FLIP auf der CSS-Eigenschaft translate, über die Web Animations API. translate
kombiniert sich mit jedem deiner transform, die Zeilenpositionierung einer virtuellen Liste
bleibt also intakt. Wenn sich ein Element erneut bewegt, bevor seine Animation endet, startet die
nächste dort, wo es gerade angezeigt wird. Dieselbe Animation bewegt die Elemente beim Drop und
beim Abbrechen.
Die animation der Liste nimmt einen Wert oder ein Ref, gelesen bei jeder Bewegung: Ein Ref
schaltet sie um, wie oben. Eine Funktion ist dort eine Animation, kein Getter.
<TransitionGroup>: Jede Bewegung würde doppelt
animiert. Für Änderungen, die kein Drag gemacht hat, schalte animateChanges
ein.Der Drop
Beim Drop fliegt das Overlay von dort, wo du losgelassen hast, zum Slot des Elements; beim
Abbrechen zurück dorthin, wo das Element gestartet ist. Wähle in der Hand oben eine
dropAnimation, ziehe dann eine Karte vom Tisch und lass los: Der Drop wird abgebrochen, und sie
fliegt zurück. toss ist eine eigene Funktion:
/** The overlay arcs to its slot, as if tossed. */
const toss: DropAnimation = ({ overlay, from, to, reducedMotion }) => {
if (reducedMotion) return;
// No visible slot to land on: fade out where it is.
if (!to) return overlay.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 200, fill: 'forwards' });
const place = (x: number, y: number) => `translate3d(${x}px, ${y}px, 0)`;
const rise = Math.min(120, Math.hypot(to.left - from.left, to.top - from.top) / 2);
return overlay.animate(
[
{ transform: place(from.left, from.top), width: `${from.width}px`, height: `${from.height}px` },
{ transform: place((from.left + to.left) / 2, Math.min(from.top, to.top) - rise), offset: 0.45 },
{ transform: place(to.left, to.top), width: `${to.width}px`, height: `${to.height}px` },
],
{ duration: 550, easing: 'ease-in-out', fill: 'forwards' },
);
};
Die Hand gibt sie ihrer Liste. Am Provider lässt sie jeden Drag landen:
<DnDProvider :drop-animation="toss">
- Die Box landet mit ihrer Position (
transform) und ihrer Größe (width,height): Der Inhalt ordnet sich in der Größe des Slots neu an, nie skaliert. - Wenn das Element nach dem Drop keinen sichtbaren Slot hat (weggescrollt oder in einen Ordner
oder eine Papierkorb-Zone abgelegt), ist
tonull. Der Standard blendet das Overlay dann aus und lässt es in die Zone schrumpfen, auf die es abgelegt wurde (into), oder an Ort und Stelle. - Bei einem Multi-Drag landen die Klone der anderen Elemente von selbst auf ihren eigenen Slots,
mit dem Standard-Timing, wenn
dropAnimationeine Funktion ist. In eine Zone gehen sie einer nach dem anderen hinein, und die Box nach ihnen (siehe die Dateien und Ordner). dropAnimationwird bei jedem Drop gelesen: Ein Ref schaltet sie für den nächsten um, wie in der Hand, und ebenso eine an ein Ref gebundene Prop des Providers.- Scrollen, während es landet, bringt es nicht vom Kurs ab. Die Box ist
position: fixed, sie folgt also dem, worauf sie landet (dem Slot, der Zone, sonst dem Ziel, auf dem sie abgelegt wurde), mit ihrem CSS-translate, zusätzlich zu demtransform, das die Animation bewegt. Animiere in deiner eigenen Animation, wie oben,transformund nichttranslate. - Der nächste Drag wartet nicht: Jeder Drop landet in einer eigenen Box, während neue Drags laufen.
Ein Drücken auf eine landende Box fängt sie im Flug, und der Drag geht von dort weiter, wo sie
gerade ist.
onDragSettlesagt dir, wann das Overlay eines Drags angekommen ist: Dann schließt der Papierkorb unten seinen Deckel.
Das Overlay nimmt beim Ziehen außerdem die Größe des Slots an, in dem es landen wird
(overlayFit: 'slot', der Standard), ein Drop braucht also selten eine Größenänderung. Siehe
Das Overlay.
Pro Element und pro Ziel
Die dropAnimation des Providers ist der Standard. Ein ziehbares Element, ein sortierbares Element
oder eine Liste (für ihre Elemente) können ihre eigene mitbringen, und ein Drop-Ziel kann das
letzte Wort darüber haben, was in ihm landet: makeDroppable(el, { overDropAnimation }), die
overDropAnimation einer Liste oder eines Baums. Für jeden Drop gilt die erste, die gesetzt ist:
- Die des Ziels (keine beim Abbrechen).
- Die eigene des gezogenen Elements.
- Die seiner Liste.
- Die des Providers.
Unten setzen der Warenkorb, die Vase und das Regal jeweils eine. Das Regal lässt seine Produkte mit einem Federn landen, die Vase hat eine eigene langsame Landung, und der Warenkorb nimmt, was auf ihm abgelegt wird, in sein Symbol auf: Das Produkt bleibt in seinem Regal, sein Slot dort ist also kein Ort zum Landen. Unter dem Regal leuchtet die auf, mit der jeder Drag landet.
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Shop from './Shop.vue';
</script>
<template>
<DnDProvider>
<Shop />
</DnDProvider>
</template>// 3. The list's, for its items without one of their own.
makeSortableList(shelf, {
items: products,
strategy: 'horizontal',
dropAnimation: { duration: 450, easing: 'cubic-bezier(.34, 1.56, .64, 1)' },
});
// 2. The item's own: it wins over its list's.
makeSortableItem(el, {
item: () => props.product,
dropAnimation: props.product.fragile ? { duration: 1100, easing: 'ease-in-out' } : undefined,
});
// 1. The target's: it wins over both.
const intoCart: DropAnimation = ({ overlay, from, reducedMotion }) => {
const end = icon.value?.getBoundingClientRect();
if (!end || reducedMotion) return;
// Into the icon, shrinking: the box scales around its top left corner.
const scale = end.height / from.height;
const animation = overlay.animate(
[
{ transform: `translate3d(${from.left}px, ${from.top}px, 0)` },
{
transform: `translate3d(${end.left + (end.width - from.width * scale) / 2}px, ${end.top}px, 0) scale(${scale})`,
opacity: 0,
},
],
{ duration: 450, easing: 'cubic-bezier(.55, 0, .75, .2)', fill: 'forwards' },
);
animation.finished.then(() => landed.value++, () => {}); // the icon bounces
return animation;
};
makeDroppable(el, { id: 'cart', overDropAnimation: intoCart, onDrop: … });
false zählt als gesetzt: Ein Ziel mit overDropAnimation: false lässt das Overlay dort, wo es
abgelegt wurde. Die Funktion bekommt das Ziel als over, so kann eine einzige Funktion Ziele
unterschiedlich behandeln.
Die eigene Landung des Inhalts
Der Drop wartet auch auf die Animationen im Overlay, die laufen, wenn er beginnt: CSS-Animationen
und -Transitions, die eine Overlay-Komponente bei status: 'dropping' startet. Über dem Papierkorb
knüllt sich ein Foto zusammen; abgelegt, wird es in den Papierkorb geworfen, und der Drop endet,
wenn es drin ist:
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Gallery from './Gallery.vue';
</script>
<template>
<DnDProvider>
<Gallery />
</DnDProvider>
</template>makeDroppable(el, {
id: 'trash',
// The overlay reads it as `over.data`: it aims the photo at the bin's mouth.
data: () => ({ bin: bin.value }),
overOverlay: Crumple,
// The box stays where it was let go: its content does the landing.
overDropAnimation: false,
onDrop: (event) => emit('drop', event), // the gallery calls `event.source?.remove()`
});
// Its landing is over: the photo is in, the lid shuts.
useDnDMonitor({
onDragSettle: (event) => {
if ('dropped' in event && event.over?.id === 'trash') shut.value++;
},
});
<template>
<div class="crumple">
<!-- `fall` is set at the drop: `--dx` and `--dy`, from the photo to the bin in `over.data`. -->
<div class="paper" :class="{ falling: fall }" :style="fall">
<slot />
</div>
</div>
</template>
<style scoped>
.paper { clip-path: polygon(…); rotate: -8deg; scale: 0.7; }
.paper.falling { animation: fall 0.55s forwards; }
@keyframes fall {
to { translate: var(--dx) var(--dy); rotate: -120deg; scale: 0.12; opacity: 0; }
}
</style>
Beendete Animationen und Endlosschleifen zählen nicht, und der Drop wartet höchstens zwei Sekunden.
Datenänderungen: animateChanges
Ziehe eine Kachel, dann mische, füge hinzu und entferne einige. Der Drag animiert standardmäßig;
die Änderungen der Buttons animieren, weil das Raster animateChanges einschaltet.
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Palette from './Palette.vue';
</script>
<template>
<DnDProvider>
<Palette />
</DnDProvider>
</template>makeSortableList(listRef, { items: swatches, animateChanges: true });
Standardmäßig aus, animiert animateChanges, was dein Array ohne einen Drag ändert: ein Sortieren, ein
Mischen, ein Verschieben zwischen Listen einer Gruppe, ein hinzugefügtes, entferntes oder
herausgefiltertes Element. In einem Baum deckt es auch die Zeilen ab, die ein Knoten beim Auf- und
Zuklappen zeigt oder verbirgt. <DnDProvider animate-changes> schaltet es für jede Liste und jeden Baum
ein.
Seine Teile lassen sich einzeln abstimmen:
animateChanges: { duration: 250, move: true, enter: true, leave: { duration: 150 } },
| Teil | Standardmäßig |
|---|---|
move | Elemente, die den Platz gewechselt haben, gleiten, wie bei animation. |
enter | Elemente, die erschienen sind, blenden sich an ihrem Platz ein. |
leave | Eine Kopie des Elements, das gegangen ist, blendet sich dort aus, wo es war, und schrumpft ein wenig. Sie ist mit data-dnd-leaving markiert und wird entfernt, wenn sie fertig ist. |
duration und easing gelten für jeden Teil; jeder Teil nimmt true, Optionen, false oder eine
Funktion. Nur Elemente in der Nähe des sichtbaren Bereichs animieren, Zeilen, die eine virtuelle
Liste nicht gemountet hat, werden also übersprungen. Die Option wird gelesen, wenn sich items
ändert, ein Ref kann sie also umschalten, wie es der Schalter der Demo tut.
Das Overlay: anheben, neigen, ablegen
Die Box des Overlays sagt, was sie gerade tut: data-dnd-overlay="dragging", während sie dem
Zeiger folgt, "dropping", während sie landet. Die Bibliothek bewegt die Box und bestimmt ihre
Größe. Darin hält data-dnd-overlay-content alles, was das Overlay zeigt – einen Klon oder einen
ganzen Stapel –, und das animierst du. Nimm einen Kandidaten auf: Das Overlay hebt sich und neigt
sich; lass los: Es legt sich beim Landen wieder flach.
Demo wird geladen…
<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Lane from './Lane.vue';
const lanes = ref([
{
id: 'applied',
title: 'Applied',
people: [
{ id: 'ava', name: 'Ava Stone', role: 'Designer', hue: 20 },
{ id: 'ben', name: 'Ben Ito', role: 'Engineer', hue: 200 },
{ id: 'cleo', name: 'Cleo Diaz', role: 'Writer', hue: 300 },
],
},
{ id: 'interview', title: 'Interview', people: [{ id: 'dan', name: 'Dan Ruiz', role: 'Engineer', hue: 140 }] },
{ id: 'hired', title: 'Hired', people: [] },
]);
</script>
<template>
<!-- `overlay-class`: the overlay's content, the whole of what follows the pointer, lifts as one. -->
<DnDProvider overlay-class="pipeline-hand">
<div class="pipeline">
<Lane v-for="lane in lanes" :key="lane.id" :lane="lane" />
</div>
</DnDProvider>
</template>
<style scoped>
.pipeline {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 12px;
width: 100%;
max-width: 600px;
margin: 0 auto;
}
</style>
<!-- Not scoped: the overlay's content element is the library's, rendered outside this component. -->
<style>
@media (prefers-reduced-motion: no-preference) {
/* Picked up, the whole overlay lifts and tilts; let go, it lies flat again as it lands. */
.pipeline-hand {
transition:
rotate 0.25s cubic-bezier(0.2, 0.8, 0.2, 1),
scale 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
}
[data-dnd-overlay='dragging'] > .pipeline-hand {
rotate: 3deg;
scale: 1.05;
}
/* Where the lift starts: the box appears already `dragging`, a transition needs a before. */
@starting-style {
[data-dnd-overlay='dragging'] > .pipeline-hand {
rotate: 0deg;
scale: 1;
}
}
}
</style><template>
<DnDProvider overlay-class="pipeline-hand">…</DnDProvider>
</template>
<!-- Not scoped: the overlay's content element is the library's, outside this component. -->
<style>
.pipeline-hand {
transition:
rotate 0.25s cubic-bezier(0.2, 0.8, 0.2, 1),
scale 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
}
[data-dnd-overlay='dragging'] > .pipeline-hand {
rotate: 3deg;
scale: 1.05;
}
/* Where the lift starts: the box appears already `dragging`. */
@starting-style {
[data-dnd-overlay='dragging'] > .pipeline-hand {
rotate: 0deg;
scale: 1;
}
}
</style>
overlay-classsetzt deine Klassen auf den Overlay-Inhalt dieses Providers: den des Drags und die, die noch landen. Tailwind nimmt es auch (starting:schreibt@starting-style).- Eine Transition braucht einen Anfang, und die Box erscheint bereits als
dragging:@starting-stylegibt dem Anheben sein „Vorher“. Wenn die Box zudroppingwird, läuft die Transition zurück, und der Drop wartet, bis sie endet. - Der Inhalt selbst animiert sich genauso, in seiner eigenen Komponente: Die Karte in der Hand und
die Spalten des Boards neigen sich, wenn sie aufgenommen werden
(
[data-dnd-overlay='dragging'] .columnmit@starting-style). Keyframes funktionieren auch: Sie spielen ab Beginn des Zustands ab. - Eine Overlay-Komponente bekommt
statusals Prop: Ihre Transitions können ihm folgen. Die Notiz aus der Demo des Provider-Slots richtet sich mittransition: rotate 0.25sauf, wennstatuszu'dropping'wird. - Lass die Box selbst (
[data-dnd-overlay]) in Ruhe: Eine Transition darauf würde dem Zeiger hinterherhinken, und die Bibliothek schreibt ihretransformund Größe.
Der Slot und die Listen
Das gezogene Element bleibt in seiner Liste als der Slot, in dem es landen wird, markiert mit
data-dnd-placeholder. Auch seine Listen wissen vom Drag: isAccepting, solange ein Drag läuft,
den sie annehmen, isOver, solange er über ihnen ist. In der Pipeline oben macht sich jede
Spalte bereit, sobald du einen Kandidaten aufnimmst, die unter der Karte leuchtet auf, ihre
Anzahl springt, und der Slot öffnet sich und atmet.
<script setup lang="ts">
const { items, isOver, isAccepting } = makeSortableList(list, {
id: props.lane.id,
items: toRef(props.lane, 'people'),
group: 'pipeline',
dropzone: zone,
});
</script>
<template>
<section ref="zone" class="lane" :class="{ ready: isAccepting, over: isOver }">
<h3>
{{ lane.title }}
<!-- A new element for every count: its animation plays on each change. -->
<span :key="items.length" class="count">{{ items.length }}</span>
</h3>
<ul ref="list">…</ul>
</section>
</template>
<style scoped>
.lane { transition: background-color 0.25s, box-shadow 0.25s; }
.lane.ready { box-shadow: inset 0 0 0 1px color-mix(in srgb, #42b883 35%, transparent); }
.lane.over { background: color-mix(in srgb, #42b883 8%, transparent); box-shadow: inset 0 0 0 1.5px #42b883; }
.count { animation: bump 0.3s ease-out; }
</style>
/* The slot opens where it appears, then breathes until the card lands. */
.candidate[data-dnd-placeholder] {
animation: open 0.2s ease-out, breathe 1.2s ease-in-out 0.2s infinite;
}
itemsist der Entwurf: Seinelengthzählt die Karte, sobald ihr Slot die Spalte betritt, die Anzahl ändert sich also, während du ziehst.- Ein Slot, der in eine andere Liste wandert, ist dort ein neues Element, also spielen seine Keyframes erneut ab.
- Die Listen selbst wachsen und schrumpfen sanft, wenn ein Slot hinein- und hinauswandert, und
das, was um sie herum ist, ebenso (
animateSize, standardmäßig an; mitfalsespringen sie). Überlass ihre Größen dem: Eine Liste, deren Größe du während eines Drags änderst, verschiebt die Ziele unter dem Zeiger. Leuchten, Umrisse, Farben undscalesind sicher.
Eigene Animationen
Jede Option nimmt auch eine Funktion. Sie bekommt einen Kontext, startet die Animation und gibt
ihre Animation zurück, ein Promise, das sich erfüllt, wenn sie endet, oder nichts. Der Drag
endet, wenn die Drop-Animation endet, und eine Kopie, die geht, wird entfernt, wenn ihre Animation
endet, aber keines von beiden wartet länger als eine Sekunde über options.duration hinaus
(250 ms für den Drop, 200 ms für die anderen).
// animation, animateChanges.move: an item moved from `from` to `to`
({ key, element, from, to, options, reason, signal, reducedMotion }) => Animation | Promise | void
// dropAnimation: `to` is null when there is no visible slot; `into` is the zone it went into
({ overlay, from, to, into, reason, active, options, reducedMotion }) => …
// animateChanges.enter: the element is already in place
({ key, element, to, options, reducedMotion }) => …
// animateChanges.leave: `element` is a copy where the item was, removed when you finish
({ key, element, from, options, reducedMotion }) => …
reason sagt, warum: 'drag' oder 'change' für eine Bewegung, 'drop' oder 'cancel' für das
Overlay. options enthält das Timing der Option, eine Funktion kann also der duration folgen,
die du daneben setzt.
Drei Regeln halten eigene Animationen im Takt mit der Bibliothek:
- Animiere
translatevonfrom - toauf null, nichttransform. Es kombiniert sich mit Transforms, die virtuelle Listen oder dein CSS auf Zeilen setzen, und Drags messen Elemente ohne es. - Beachte
signal. Es bricht ab, wenn sich das Element erneut bewegt, bevor die Animation endet. Stoppe deine Animation und entferne ihre Stile sofort: Das Layout wird direkt danach gemessen. Eine zurückgegebeneAnimationwird für dich abgebrochen. - Lande auf einem Slot mit Position und Größe (
width,height), nicht mitscale: Der Inhalt des Overlays ordnet sich in der Größe des Slots neu an. Wo es keinen Slot zum Landen gibt, ist Schrumpfen in Ordnung: Der Standard verkleinert das Overlay in die Zone (into), der Warenkorb oben in sein Symbol.
motion
Die Girlande unten läuft mit motion: Die Fähnchen und der Drop federn über die Funktionen des Providers, und die Overlay-Komponente schwingt mit der Geschwindigkeit des Zeigers.
Demo wird geladen…
<script setup lang="ts">
import { animate } from 'motion-v';
import { DnDProvider, type DropAnimationContext, type LayoutAnimationContext } from '@vue-dnd-kit/core';
import Garland from './Garland.vue';
const spring = { type: 'spring', duration: 0.5, bounce: 0.3 } as const;
/** Flags spring to their new places, on the CSS `translate` property. */
const move = ({ element, from, to, signal, reducedMotion }: LayoutAnimationContext) => {
if (reducedMotion) return;
const controls = animate(element, { translate: [`${from.left - to.left}px ${from.top - to.top}px`, '0px 0px'] }, spring);
// The flag moves again before it arrives: stop and clear at once, the library measures next.
signal.addEventListener('abort', () => {
controls.stop();
element.style.translate = '';
});
return controls.finished;
};
/** The overlay springs onto its slot: its position and its size. */
const drop = ({ overlay, from, to, reducedMotion }: DropAnimationContext) => {
if (reducedMotion) return;
if (!to) return animate(overlay, { opacity: 0 }, { duration: 0.18 }).finished;
return animate(
overlay,
{ x: [from.left, to.left], y: [from.top, to.top], width: [from.width, to.width], height: [from.height, to.height] },
spring,
).finished;
};
</script>
<template>
<DnDProvider :animation="move" :drop-animation="drop">
<Garland />
</DnDProvider>
</template><script setup lang="ts">
import { animate } from 'motion-v';
import { DnDProvider, type DropAnimationContext, type LayoutAnimationContext } from '@vue-dnd-kit/core';
const spring = { type: 'spring', duration: 0.5, bounce: 0.3 } as const;
const move = ({ element, from, to, signal, reducedMotion }: LayoutAnimationContext) => {
if (reducedMotion) return;
const controls = animate(element, { translate: [`${from.left - to.left}px ${from.top - to.top}px`, '0px 0px'] }, spring);
signal.addEventListener('abort', () => {
controls.stop();
element.style.translate = '';
});
return controls.finished;
};
const drop = ({ overlay, from, to, reducedMotion }: DropAnimationContext) => {
if (reducedMotion) return;
if (!to) return animate(overlay, { opacity: 0 }, { duration: 0.18 }).finished;
return animate(
overlay,
{ x: [from.left, to.left], y: [from.top, to.top], width: [from.width, to.width], height: [from.height, to.height] },
spring,
).finished;
};
</script>
<template>
<DnDProvider :animation="move" :drop-animation="drop">
<Garland />
</DnDProvider>
</template>
Die Overlay-Komponente rendert unter dem Provider, also funktioniert useDnD() in ihr. Sie folgt
dem Zeiger über einen Motion Value:
<script setup lang="ts">
import { motion, useMotionValue, useReducedMotion, useSpring, useTransform, useVelocity } from 'motion-v';
import { useDnD, type DnDStatus, type DragItem } from '@vue-dnd-kit/core';
const props = defineProps<{ items: DragItem[]; status: DnDStatus }>();
const { pointer } = useDnD();
const x = useMotionValue(pointer.value?.x ?? 0);
watch(() => pointer.value?.x, (value) => value !== undefined && x.set(value));
const swing = useSpring(useTransform(useVelocity(x), [-1500, 0, 1500], [-25, 0, 25]), { stiffness: 300, damping: 14 });
const calm = useReducedMotion();
</script>
<template>
<!-- Held by its top edge: `transform-origin: 50% 0` in its CSS. -->
<motion.div
class="flag-overlay"
:style="{ rotate: calm ? 0 : swing }"
:animate="{ scale: status === 'dropping' || calm ? 1 : 1.1 }"
>
…
</motion.div>
</template>
GSAP
import gsap from 'gsap';
import type {
DropAnimationContext,
EnterAnimationContext,
LayoutAnimationContext,
LeaveAnimationContext,
Rect,
} from '@vue-dnd-kit/core';
const move = ({ element, from, to, signal, reducedMotion }: LayoutAnimationContext) => {
if (reducedMotion) return;
const tween = gsap.fromTo(
element,
{ translate: `${from.left - to.left}px ${from.top - to.top}px` },
{ translate: '0px 0px', duration: 0.4, ease: 'power3.out' },
);
signal.addEventListener('abort', () => tween.revert());
return tween.then();
};
const box = (rect: Rect) => ({ x: rect.left, y: rect.top, width: rect.width, height: rect.height });
const drop = ({ overlay, from, to, reducedMotion }: DropAnimationContext) => {
if (reducedMotion) return;
if (!to) return gsap.to(overlay, { opacity: 0, duration: 0.2 }).then();
return gsap.fromTo(overlay, box(from), { ...box(to), duration: 0.45, ease: 'back.out(1.2)' }).then();
};
const enter = ({ element, reducedMotion }: EnterAnimationContext) =>
gsap.from(element, reducedMotion ? { opacity: 0, duration: 0.2 } : { opacity: 0, scale: 0.85, duration: 0.35 }).then();
const leave = ({ element, reducedMotion }: LeaveAnimationContext) =>
gsap.to(element, reducedMotion ? { opacity: 0, duration: 0.2 } : { opacity: 0, scale: 0.85, duration: 0.25 }).then();
export const gsapAnimations = { animation: move, dropAnimation: drop, animateChanges: { move, enter, leave } };
<DnDProvider v-bind="gsapAnimations">
Die vollständigen Typen der Optionen stehen unter Animationen.
Reduzierte Bewegung
Bei prefers-reduced-motion: reduce laufen die eigenen Animationen der Bibliothek nicht: Elemente
und das Overlay gehen direkt an ihre Plätze, und das Overlay gleitet nicht zwischen Slot-Größen.
Deine Funktionen laufen immer, mit reducedMotion: true, sie entscheiden also: die Bewegung
auslassen oder ein kurzes Ausblenden behalten, wie es die Rezepte oben tun.
Dein CSS sicherst du selbst ab. Setz Anheben, Neigen und Pulsieren in
@media (prefers-reduced-motion: no-preference), wie es die Demos auf dieser Seite tun, und lass
die Farbwechsel außerhalb davon: Sie zeigen, wohin ein Drop geht.
Layoutänderungen, die nicht von der Bibliothek kommen
Wenn etwas anderes während eines Drags Zeilen verschiebt – ein Virtualizer, der neu misst, ein
Bild, das lädt –, ruf captureLayout() der Liste direkt vor dem Rendern der Änderung auf. Es misst
die Zeilen dort, wo sie sind, und animiert sie nach dem nächsten Rendern an ihre neuen Plätze.
Außerhalb eines Drags tut es nichts. Siehe Virtuelle Listen.