Documentation/Guides
Animations
Les items glissent pendant le tri, le dépôt atterrit sur son emplacement, et les changements de données peuvent aussi s'animer. Réglez chacun, animez l'overlay, l'emplacement et les listes en CSS, ou utilisez motion ou GSAP.
Cette page a été traduite par IA et peut contenir des inexactitudes. Lire l'original en anglais
Trois choses s'animent d'elles-mêmes : les items qui glissent pendant le tri, l'overlay qui atterrit au dépôt et, si vous l'activez, les changements de vos données qui ne viennent pas d'un glissement. Chacune accepte les mêmes valeurs, sur le provider ou plus près de ce qu'elle anime :
| Option | Où | Par défaut | Ce qu'elle anime |
|---|---|---|---|
animation | provider, liste, arbre | activée, 200 ms | Les items qui glissent vers leurs nouvelles places pendant le tri, au dépôt et à l'annulation. |
dropAnimation | provider, élément déplaçable, item, liste, arbre (overDropAnimation sur une cible de dépôt) | activée, 250 ms | L'overlay qui atterrit sur son emplacement, ou qui revient à sa place à l'annulation. |
animateChanges | provider, liste, arbre | désactivée | Les changements de vos données qui ne viennent pas d'un glissement : items ajoutés, supprimés, déplacés ou filtrés, lignes d'arbre affichées ou masquées. |
Chacune accepte true (le timing par défaut), false (désactivée), { duration, easing }, ou
une fonction qui fait l'animation vous-même :
<DnDProvider
:animation="{ duration: 180, easing: 'cubic-bezier(.2, .8, .2, 1)' }"
:drop-animation="{ duration: 300 }"
animate-changes
>
Les animation et animateChanges propres à une liste ou à un arbre l'emportent sur ceux du provider ;
pour dropAnimation, voir par item et par cible. Le reste du
mouvement vous appartient : l'overlay, l'emplacement et les listes sont des éléments avec des
attributs data et des refs, et le CSS ou une bibliothèque d'animation
les fait bouger.
Tri
Pendant le glissement, le brouillon de la liste se réordonne et les items glissent vers leurs
nouvelles places. La main ci-dessous passe les deux options à sa liste : choisissez une
animation et triez les cartes, choisissez une dropAnimation et lâchez-en une.
Chargement de la démo…
<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,
});
C'est du FLIP sur la propriété CSS translate, via la Web Animations API. translate se compose
avec n'importe lequel de vos transform : le positionnement des lignes d'une liste virtuelle
reste donc intact. Quand un item bouge de nouveau avant la fin de son animation, la suivante part
de l'endroit où il est affiché. La même animation déplace les items au dépôt et à l'annulation.
L'animation de la liste accepte une valeur ou une ref, lue à chaque déplacement : une ref la
change, comme ci-dessus. Une fonction à cet endroit est une animation, pas un getter.
<TransitionGroup> : chaque déplacement s'animerait
deux fois. Pour les changements qui ne viennent pas d'un glissement, activez
animateChanges.Le dépôt
Au dépôt, l'overlay vole de l'endroit où vous avez relâché jusqu'à l'emplacement de l'item ; à
l'annulation, jusqu'à l'endroit où l'item a commencé. Dans la main ci-dessus, choisissez une
dropAnimation, puis faites glisser une carte hors de la table et relâchez : le dépôt est annulé,
et elle revient à sa place. toss est une fonction à elle :
/** 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' },
);
};
La main la donne à sa liste. Sur le provider, elle fait atterrir chaque glissement :
<DnDProvider :drop-animation="toss">
- La boîte atterrit avec sa position (
transform) et sa taille (width,height) : le contenu se remet en page à la taille de l'emplacement, jamais mis à l'échelle. - Quand l'item n'a pas d'emplacement visible après le dépôt (défilé hors de vue, ou déposé dans
un dossier ou une corbeille),
tovautnull. L'animation par défaut estompe alors l'overlay, en le rétrécissant dans la zone où il a été déposé (into), ou sur place. - Dans un multi-drag, les clones des autres items atterrissent d'eux-mêmes sur leurs propres
emplacements, avec le timing par défaut quand
dropAnimationest une fonction. Dans une zone, ils entrent l'un après l'autre, et la boîte après eux (voir les fichiers et les dossiers). dropAnimationest lue à chaque dépôt : une ref la change pour le suivant, comme dans la main, et une prop du provider liée à une ref aussi.- Défiler pendant qu'il atterrit ne le fait pas dévier. La boîte est en
position: fixed: elle suit donc ce sur quoi elle atterrit (l'emplacement, la zone, sinon la cible où elle a été déposée) par sontranslateCSS, en plus dutransformque fait bouger l'animation. Dans la vôtre, animeztransform, comme ci-dessus, pastranslate. - Le glissement suivant n'attend pas : chaque dépôt atterrit dans une boîte à lui pendant que de
nouveaux glissements se déroulent. Un appui sur une boîte en train d'atterrir la rattrape en
plein vol, et le glissement reprend là où elle se trouve.
onDragSettleindique quand l'overlay d'un glissement est arrivé à destination : c'est alors que la corbeille ci-dessous ferme son couvercle.
L'overlay prend aussi la taille de l'emplacement où il va atterrir pendant le glissement
(overlayFit: 'slot', la valeur par défaut) : un dépôt a donc rarement besoin d'un
redimensionnement. Voir l'overlay.
Par item et par cible
La dropAnimation du provider est celle par défaut. Un élément déplaçable, un item triable ou une
liste (pour ses items) peuvent apporter la leur, et une cible de dépôt peut avoir le dernier mot
sur ce qui atterrit en elle : makeDroppable(el, { overDropAnimation }), l'overDropAnimation d'une
liste ou d'un arbre. Pour chaque dépôt, la première qui est définie :
- Celle de la cible (aucune en cas d'annulation).
- Celle de l'item glissé lui-même.
- Celle de sa liste.
- Celle du provider.
Ci-dessous, le panier, le vase et l'étagère en définissent chacun une. L'étagère fait atterrir ses produits avec un rebond, le vase a un atterrissage lent à lui, et le panier fait entrer dans son icône ce qui est déposé dessus : le produit reste sur son étagère, son emplacement là-bas n'est donc pas un endroit où atterrir. Sous l'étagère, celle avec laquelle chaque glissement atterrit s'allume.
Chargement de la démo…
<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 compte comme définie : une cible avec overDropAnimation: false laisse l'overlay là où il a
été déposé. La fonction reçoit la cible en over : une seule fonction peut donc traiter chaque
cible à sa façon.
L'atterrissage propre au contenu
Le dépôt attend aussi les animations à l'intérieur de l'overlay qui sont en cours quand il
commence : les animations et transitions CSS qu'un composant d'overlay lance sur
status: 'dropping'. Au-dessus de la corbeille, une photo se froisse ; déposée, elle est jetée
dans la corbeille, et le dépôt se termine quand elle y est :
Chargement de la démo…
<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>
Les animations terminées et les boucles infinies ne comptent pas, et le dépôt attend deux secondes au plus.
Changements de données : animateChanges
Faites glisser une tuile, puis mélangez, ajoutez-en et supprimez-en. Le glissement s'anime par
défaut ; les changements des boutons s'animent parce que la grille active animateChanges.
Chargement de la démo…
<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 });
Désactivé par défaut, animateChanges anime ce qui change votre tableau sans glissement : un tri, un
mélange, un déplacement entre les listes d'un même groupe, un item ajouté, supprimé ou filtré.
Dans un arbre, il couvre aussi les lignes qu'un nœud affiche ou masque quand il se déplie et se
replie. <DnDProvider animate-changes> l'active pour chaque liste et chaque arbre.
Ses parties peuvent être réglées une à une :
animateChanges: { duration: 250, move: true, enter: true, leave: { duration: 150 } },
| Partie | Par défaut |
|---|---|
move | Les items qui ont changé de place glissent, comme avec animation. |
enter | Les items apparus apparaissent en fondu là où ils sont. |
leave | Une copie de l'item parti disparaît en fondu là où il était, en rétrécissant un peu. Elle est marquée data-dnd-leaving et retirée quand elle a fini. |
duration et easing s'appliquent à toutes les parties ; chaque partie accepte true, des
options, false ou une fonction. Seuls les items proches de la zone visible s'animent : les
lignes qu'une liste virtuelle n'a pas montées sont donc ignorées. L'option est lue quand items
change : une ref peut donc la basculer, comme le fait l'interrupteur de la démo.
L'overlay : soulever, incliner, reposer
La boîte de l'overlay dit ce qu'elle fait : data-dnd-overlay="dragging" pendant qu'elle suit
le pointeur, "dropping" pendant qu'elle atterrit. La bibliothèque déplace et dimensionne la
boîte. À l'intérieur, data-dnd-overlay-content contient tout ce que l'overlay montre, un clone
ou toute une pile, et c'est à vous de l'animer. Saisissez un candidat : l'overlay se soulève et
s'incline ; relâchez : il se remet à plat en atterrissant.
Chargement de la démo…
<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-classmet vos classes sur le contenu des overlays de ce provider : celui du glissement et ceux qui atterrissent encore. Tailwind l'accepte aussi (starting:écrit@starting-style).- Une transition a besoin d'un départ, et la boîte apparaît déjà
dragging:@starting-styledonne au soulèvement son « avant ». Quand la boîte passe àdropping, la transition revient en arrière, et le dépôt attend qu'elle se termine. - Le contenu lui-même s'anime de la même façon, dans son propre composant : la carte en main et
les colonnes du tableau s'inclinent quand on les saisit
(
[data-dnd-overlay='dragging'] .columnavec@starting-style). Les keyframes fonctionnent aussi : elles se jouent dès le début de l'état. - Un composant d'overlay reçoit
statusen prop : ses transitions peuvent le suivre. La note de la démo du slot du provider se redresse avectransition: rotate 0.25squandstatuspasse à'dropping'. - Laissez la boîte elle-même (
[data-dnd-overlay]) tranquille : une transition sur elle traînerait derrière le pointeur, et la bibliothèque écrit sontransformet sa taille.
L'emplacement et les listes
L'élément glissé reste dans sa liste comme l'emplacement où il va atterrir, marqué
data-dnd-placeholder. Ses listes sont aussi au courant du glissement : isAccepting pendant un
glissement qu'elles acceptent, isOver pendant qu'il est au-dessus d'elles. Dans le pipeline
ci-dessus, chaque colonne se prépare dès que vous saisissez un candidat, celle sous la carte
s'éclaire, son compteur saute, et l'emplacement s'ouvre et respire.
<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;
}
itemsest le brouillon : salengthcompte la carte dès que son emplacement entre dans la colonne, si bien que le compteur change pendant le glissement.- Un emplacement qui passe dans une autre liste y est un nouvel élément : ses keyframes se rejouent donc.
- Les listes elles-mêmes grandissent et rétrécissent en douceur quand un emplacement entre et
sort, et ce qui les entoure aussi (
animateSize, activé par défaut ; avecfalse, elles sautent). Laissez-lui leurs tailles : une liste que vous redimensionnez pendant un glissement déplace les cibles sous le pointeur. Les halos, les contours, les couleurs etscalesont sans risque.
Vos propres animations
Chaque option accepte aussi une fonction. Elle reçoit un contexte, démarre l'animation et
renvoie son Animation, une promesse qui se règle quand elle se termine, ou rien. Le glissement
se termine quand l'animation de dépôt se termine, et une copie sortante est retirée quand son
animation se termine, mais aucun des deux n'attend plus d'une seconde au-delà de
options.duration (250 ms pour le dépôt, 200 ms pour les autres).
// 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 dit pourquoi : 'drag' ou 'change' pour un déplacement, 'drop' ou 'cancel' pour
l'overlay. options contient le timing de l'option : une fonction peut donc suivre la duration
que vous réglez à côté.
Trois règles gardent les animations personnalisées en phase avec la bibliothèque :
- Animez
translatedefrom - tojusqu'à zéro, pastransform. Il se compose avec les transformations que les listes virtuelles ou votre CSS posent sur les lignes, et les glissements mesurent les items sans lui. - Respectez
signal. Il s'interrompt quand l'item bouge de nouveau avant la fin de l'animation. Arrêtez votre animation et effacez ses styles tout de suite : la mise en page est mesurée juste après. UneAnimationrenvoyée est annulée pour vous. - Atterrissez sur un emplacement avec la position et la taille (
width,height), passcale: le contenu de l'overlay se remet en page à la taille de l'emplacement. Là où il n'y a pas d'emplacement où atterrir, rétrécir convient : l'animation par défaut réduit l'overlay dans la zone (into), et le panier ci-dessus dans son icône.
motion
La guirlande ci-dessous tourne sur motion : les fanions et le dépôt rebondissent via les fonctions du provider, et le composant d'overlay se balance selon la vitesse du pointeur.
Chargement de la démo…
<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>
Le composant d'overlay est rendu sous le provider : useDnD() y fonctionne donc. Il suit le
pointeur via une 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">
Les types complets des options se trouvent dans Animations.
Mouvement réduit
Avec prefers-reduced-motion: reduce, les animations propres à la bibliothèque ne s'exécutent
pas : les items et l'overlay vont directement à leur place, et l'overlay ne passe pas en douceur
d'une taille d'emplacement à l'autre. Vos fonctions s'exécutent toujours, avec
reducedMotion: true : c'est donc elles qui décident, sauter le mouvement ou garder un bref
fondu, comme le font les recettes ci-dessus.
Votre CSS, c'est à vous de le protéger. Placez les soulèvements, les inclinaisons et les
pulsations dans @media (prefers-reduced-motion: no-preference), comme le font les démos de
cette page, et gardez les changements de couleur en dehors : ils indiquent où va un dépôt.
Changements de mise en page que la bibliothèque n'a pas causés
Quand autre chose déplace des lignes pendant un glissement — un virtualiseur qui remesure, une
image qui se charge —, appelez le captureLayout() de la liste juste avant que le changement ne
soit rendu. Il mesure les lignes là où elles sont et les anime vers leurs nouvelles places après
le rendu suivant. En dehors d'un glissement, il ne fait rien. Voir les
listes virtuelles.