Documentation/Guides
Style
Chaque état d'un glissement, stylisé — des attributs data pour le CSS simple, des refs pour les classes, des cibles qui disent oui ou non, l'overlay et la page pendant le glissement, les styles par défaut, Tailwind et les kits d'UI.
Cette page a été traduite par IA et peut contenir des inexactitudes. Lire l'original en anglais
La bibliothèque ne stylise presque rien. Chaque état d'un glissement vous parvient de deux
façons : sous forme d'attribut data-dnd-*, pour le CSS simple, et sous forme de ref renvoyée
par le composable, pour les classes et le balisage conditionnel. Utilisez celle qui convient à
votre stack, ou les deux.
Les attributs data
Les lignes ci-dessous n'ont aucune classe d'état : chaque aspect vient d'un attribut. Le panneau liste les attributs de la page à mesure qu'ils apparaissent et disparaissent. Sélectionnez deux lignes avec Ctrl ou Meta et faites-les glisser : les autres d'un multi-drag apparaissent aussi.
Chargement de la démo…
<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 | Sur | Quand |
|---|---|---|
data-dnd-placeholder | un item triable | C'est l'emplacement où un item glissé va atterrir. "into" pendant que le glissement est au-dessus d'une cible de dépôt dans un item : le dépôt va là, et l'emplacement ne fait que garder sa place. |
data-dnd-dragging | un item triable | Il est glissé mais reste en place : les autres items d'un multi-drag en mode 'gaps'. |
data-dnd-dragging="pointer" ou "keyboard" | <html> | Pendant tout le glissement. |
data-dnd-overlay="dragging" ou "dropping" | la boîte de l'overlay | Toujours. dropping pendant qu'elle atterrit, et pendant qu'un onBeforeDrop asynchrone attend. |
data-dnd-overlay-content | le contenu de l'overlay | Toujours : l'élément qui remplit la boîte et contient ce qu'elle montre. C'est à vous de l'animer ; il reçoit l'overlayClass du provider. |
data-dnd-leaving | une copie laissée par animateChanges | Pendant qu'un item parti disparaît en fondu. |
data-dnd-no-clone | posé par vous | Exclut l'élément de la copie de l'overlay : une infobulle, un menu, un 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>
- La copie d'une ligne dans l'overlay garde ses classes et ses attributs, y compris l'attribut de
scope de Vue :
[data-dnd-overlay] .rowfonctionne donc dans le propre<style scoped>de la ligne. La copie n'a pas de marquesdata-dnd-*à elle : une copie de l'emplacement n'est pas un emplacement. - La sélection n'a pas d'attribut. Reportez la ref
selecteddans un attribut (aria-selectedci-dessus), ou utilisez une classe. - Les éléments déplaçables simples et les cibles de dépôt (
makeDraggable,makeDroppable) ne reçoivent pas d'attributs d'étatdata-dnd-*: stylisez-les avec leurs refs.
Les refs renvoyées
Les mêmes états sous forme de refs, plus ceux des listes et des cibles. Chaque ligne et chaque liste ci-dessous affiche les refs qui sont vraies à cet instant, et ses classes en découlent. L'Icebox refuse les tâches urgentes : faites-en glisser une au-dessus.
Chargement de la démo…
<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 | État |
|---|---|
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: cet élément est l'emplacement où un item glissé va atterrir. Dans un multi-drag en mode'group', les emplacements des items qui voyagent avec lui sont aussi des placeholders.isDragging: l'item fait partie du glissement ; chaque item glissé dans un multi-drag.isAccepting: quelque chose est en cours de glissement, et cette cible l'accepte. Mettez en valeur les endroits où un dépôt est possible.isOver: le glissement est au-dessus de cette cible, qu'elle l'accepte ou non.isOver && !isAcceptingest un refus : montrez-le.indicator: l'endroit où va la ligne de dépôt en mode indicateur.activation.progress: de 0 à 1 pendant un appui long tactile ou undelay. Remplissez un anneau avec.
Cibles de dépôt : oui et non
Une cible a quatre aspects pendant un glissement : elle accepte le glissement, elle l'accepte et le pointeur est dessus, elle le refuse, elle le refuse et le pointeur est dessus. Montrez les quatre, et dites-le aussi sur l'item glissé.
Chargement de la démo…
<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 pastille glissée est le slot #overlay du provider. Il reçoit over et accepted :
accepted vaut false au-dessus d'une cible qui refuse le glissement.
<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>
En dehors de l'overlay, useDnD().over.value?.accepted dit la même chose. Plus de détails sur
les composants d'overlay dans l'overlay.
L'overlay et la page
Deux attributs couvrent le reste d'un glissement : data-dnd-overlay sur la boîte de l'overlay,
et data-dnd-dragging sur <html>, "pointer" ou "keyboard". La barre au-dessus des notes lit
le second, et indique quelles touches fonctionnent. Activez l'enregistrement lent : l'overlay
reste en dropping là où vous avez relâché, jusqu'à ce que onBeforeDrop se résolve, et le
montre.
Chargement de la démo…
<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; }
droppingcouvre l'atterrissage et l'attente d'unonBeforeDropasynchrone. Pendant cette attente,<html>est sorti du mode glissement : le curseur et la sélection de texte de la page sont revenus, et une boîte de dialogue de confirmation fonctionne donc normalement.- Stylisez ce qui est à l'intérieur de la boîte. La bibliothèque déplace la boîte elle-même
avec
transformet la dimensionne avecwidthetheight. Son élément de contenu,[data-dnd-overlay-content], la remplit et vous appartient : soulevez et inclinez-y l'overlay entier (voir les animations). - La racine de la copie a
transform: noneen ligne : faites-la tourner ou grandir avecrotateetscale. Les transitions comme les keyframes s'y exécutent : voir les animations. - La copie montre l'élément tel qu'il est et ne devine aucun fond. Une ligne que la liste peint
(un
<li>dans un<ul>blanc, une ligne de tableau) en a besoin :[data-dnd-overlay] .row { background: Canvas; }. - La copie se trouve dans un
<div>qui porte les propriétés personnalisées dont l'élément héritait (couleurs du thème,--tw-*) : elles s'appliquent donc toujours dans l'overlay, et les règles propres à la copie (in-data-[dnd-overlay]:ring-2) l'emportent toujours sur elles. Sélectionnez la copie par sa classe, pas par sa profondeur dans la boîte.
Styles par défaut
Le provider injecte quelques règles, une fois par document. Elles sont enveloppées dans
:where() : leur spécificité est donc nulle et la moindre de vos règles l'emporte :
: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;
}
| Règle | Pourquoi |
|---|---|
| L'emplacement à 40 % | Pour qu'une liste ait l'air correcte sans aucun CSS. Chaque démo ci-dessus la remplace. |
| Les items laissés en place à 40 % | La même chose, pour un multi-drag en mode 'gaps'. |
Pas de user-select | Un glissement ne sélectionne pas le texte qu'il survole. |
Pas d'overflow-anchor | La page ne défile pas pour garder une ligne en mouvement visible. |
| Le curseur du glissement | Pendant un glissement au pointeur, partout. grabbing, sauf si l'élément saisi a un curseur à lui : le ns-resize d'une poignée de redimensionnement, un move, une image gardent le leur pendant tout le glissement (grab, pointer et consorts deviennent quand même grabbing). cursor: grab sur les items déplaçables, c'est à vous de l'ajouter. Posez --dnd-cursor sur <html> pour un autre curseur par défaut. |
| Pas de menu contextuel tactile | iOS n'ouvre pas le menu des liens sous un appui long. |
:default-styles="false" sur le provider les omet. La bibliothèque marque toujours <html>, et
efface toujours la sélection quand un glissement commence et empêche d'en créer une nouvelle jusqu'à sa fin :
seul le CSS disparaît. Les règles sont injectées une fois, par le premier provider qui les
demande, et restent : désactivez-les sur chaque provider de la page.
<DnDProvider :default-styles="false">
Tailwind CSS
Les attributs fonctionnent comme des variantes : data-[…] pour l'élément lui-même, une
variante arbitraire pour un ancêtre.
<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, déclarée avant toutes les autres. Les utilitaires de Tailwind v4 et les kits d'UI
construits dessus vivent aussi dans des layers, qui viennent après : ils l'emportent donc sans
!. Le CSS hors layer l'emporte de toute façon.Dans Tailwind v4, in-data-[dnd-overlay=dragging]:shadow-xl est une façon plus courte d'écrire
la variante d'ancêtre.
:global() prend tout le sélecteur : :global(html[data-dnd-dragging]) .hint
se compile en html[data-dnd-dragging] seul, et masque la page. Écrivez l'ancêtre sans lui,
html[data-dnd-dragging] .hint : Vue ne scope que la dernière partie.Kits d'UI
- Les composables acceptent un composant aussi bien qu'un élément :
useTemplateRefsur une carte ou un élément de liste d'un kit fonctionne, et la bibliothèque lit son élément racine. - Passez l'état aux props propres du kit quand il en a (
:active="selected",:color="isOver ? 'primary' : undefined"), et le reste sous forme de classes. - Les éléments de liste d'un kit n'ont souvent pas de fond à eux : c'est la liste qui le peint.
Leur copie dans l'overlay est alors transparente. Donnez-lui-en un :
[data-dnd-overlay] .v-list-item { background: rgb(var(--v-theme-surface)); } - La copie laisse de côté les ondulations d'appui de Vuetify, Quasar, PrimeVue et Naive UI.
Marquez tout ce qui ne doit pas voyager, une infobulle ou un bouton de menu, avec
data-dnd-no-clone.
À éviter
- Ne mettez pas de
transitionCSS sur letransformou letranslatedes items triables : la bibliothèque les anime elle-même, et annule ces transitions pendant qu'elle mesure. - N'enveloppez pas les items triables dans un
<TransitionGroup>: les déplacements s'animeraient deux fois. Utilisez l'animateChangesde la liste. - N'animez pas la
height, lamarginou lepaddingdes lignes d'une liste virtuelle. - En revanche, arrêtez sous
prefers-reduced-motion: reduceles transitions que vous ajoutez autour des glissements. La bibliothèque arrête déjà les siennes.