ライブラリはほとんど何もスタイルを付けません。ドラッグの各状態は 2 つの方法で届きます:素の CSS 向けの data-dnd-* 属性と、クラスや条件付きのマークアップ向けにコンポーザブルが返す ref です。スタックに合うほうを、あるいは両方を使ってください。
data 属性
下の行には状態用のクラスがまったくありません。見た目はすべて属性から来ています。パネルには、ページ上で付いたり外れたりする属性が表示されます。:kbd{value="Ctrl"} か Meta で 2 行を選んでドラッグすると、マルチドラッグのほかのアイテムも現れます。
デモを読み込み中…
<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>| 属性 | 付く場所 | タイミング |
|---|---|---|
data-dnd-placeholder | 並べ替え可能なアイテム | ドラッグ中のアイテムが着地する枠であるとき。ドラッグがアイテムの中のドロップターゲットの上にある間は "into":ドロップはそちらに入り、枠は位置を保っているだけです。 |
data-dnd-dragging | 並べ替え可能なアイテム | ドラッグされているが、その場にとどまっているとき:'gaps' モードのマルチドラッグのほかのアイテム。 |
data-dnd-dragging="pointer" または "keyboard" | <html> | ドラッグの間ずっと。 |
data-dnd-overlay="dragging" または "dropping" | オーバーレイのボックス | 常に。着地中と、非同期の onBeforeDrop を待っている間は dropping。 |
data-dnd-overlay-content | オーバーレイのコンテンツ | 常に。ボックスを満たし、表示するものを包む要素。自由にアニメーションできます。プロバイダーの overlayClass が付きます。 |
data-dnd-leaving | animateChanges が残すコピー | 去ったアイテムがフェードアウトしている間。 |
data-dnd-no-clone | あなたが付ける | 要素をオーバーレイのコピーから除外します:ツールチップ、メニュー、バッジ。 |
/* 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>
- オーバーレイにある行のコピーは、Vue のスコープ属性も含めてクラスと属性を保つので、
[data-dnd-overlay] .rowは行自身の<style scoped>でも機能します。コピーには独自のdata-dnd-*マークがありません:枠のコピーは枠ではありません。 - 選択には属性がありません。
selectedref を属性(上のaria-selected)に反映させるか、クラスを使ってください。 - 通常のドラッグ可能な要素とドロップターゲット(
makeDraggable、makeDroppable)にはdata-dnd-*の状態属性が付きません。それらの ref でスタイルを付けてください。
返される ref
同じ状態が ref としても提供され、さらにリストとターゲットに固有の状態も加わります。下の各行と各リストは、今 true になっている ref を表示し、クラスもそこから来ています。Icebox は緊急のタスクを拒否します。1 つをその上へドラッグしてみましょう。
デモを読み込み中…
<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>
| コンポーザブル | 状態 |
|---|---|
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:この要素が、ドラッグ中のアイテムが着地する枠です。'group'モードのマルチドラッグでは、一緒に移動するアイテムの枠もプレースホルダーです。isDragging:アイテムがドラッグの一部です。マルチドラッグでは、ドラッグ中のすべてのアイテムが該当します。isAccepting:何かがドラッグされていて、このターゲットがそれを受け入れます。ドロップできる場所を強調表示しましょう。isOver:受け入れるかどうかに関係なく、ドラッグがこのターゲットの上にあります。isOver && !isAcceptingは拒否です。それを表示しましょう。indicator:インジケーターモードで、ドロップのラインが入る位置。activation.progress:タッチの長押しやdelayの間、0 から 1 まで進みます。リングを塗るのに使えます。
ドロップターゲット:受け入れと拒否
ドラッグ中、ターゲットには 4 つの見た目があります:ドラッグを受け入れる、受け入れてポインターが上にある、拒否する、拒否してポインターが上にある。4 つすべてを表示し、ドラッグ中のアイテムでもそれを伝えましょう。
デモを読み込み中…
<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 */
ドラッグ中の色見本はプロバイダーの #overlay スロットです。スロットは over と accepted を受け取ります。ドラッグを拒否するターゲットの上では accepted は false です。
<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>
オーバーレイの外では、useDnD().over.value?.accepted が同じことを伝えます。オーバーレイコンポーネントについて詳しくはオーバーレイを参照してください。
オーバーレイとページ
ドラッグの残りは 2 つの属性でカバーできます:オーバーレイのボックスの data-dnd-overlay と、<html> の data-dnd-dragging("pointer" または "keyboard")です。メモの上のバーは 2 つ目を読み取り、どのキーが使えるかを表示します。遅い保存をオンにしてみましょう。オーバーレイは onBeforeDrop が解決するまで離した場所で dropping のままになり、それを表示します。
デモを読み込み中…
<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; }
droppingは、着地と非同期のonBeforeDropの待機の両方をカバーします。待機中、<html>はドラッグモードを抜けます。ページのカーソルとテキスト選択が戻るので、確認ダイアログはいつもどおり動作します。- ボックスの中身にスタイルを当ててください。ボックス自体はライブラリが
transformで動かし、widthとheightでサイズを決めます。そのコンテンツ要素[data-dnd-overlay-content]はボックスを満たしていて、自由に使えます:オーバーレイ全体の持ち上げや傾きはここで付けてください(アニメーションを参照)。 - コピーのルートにはインラインで
transform: noneが付いています。回転や拡大にはrotateとscaleを使ってください。トランジションとキーフレームのどちらも動作します:アニメーションを参照。 - コピーは要素をそのまま表示し、背景を推測しません。リストが背景を塗っている行(白い
<ul>の中の<li>、テーブルの行)には背景が必要です:[data-dnd-overlay] .row { background: Canvas; }。 - コピーは、要素が継承していたカスタムプロパティ(テーマの色、
--tw-*)を持つ<div>の中に置かれます。そのためオーバーレイの中でもそれらが適用され、コピー自身のルール(in-data-[dnd-overlay]:ring-2)は引き続きそれらに勝ちます。コピーは、ボックス内での深さではなくクラスで選択してください。
デフォルトのスタイル
プロバイダーは、ドキュメントごとに一度、いくつかのルールを挿入します。それらは :where() で囲まれているので詳細度はゼロで、あなたのどんなルールも優先されます。
: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;
}
| ルール | 理由 |
|---|---|
| 枠を 40% に | CSS がまったくなくてもリストが正しく見えるように。上のデモはすべてこれを上書きしています。 |
| 残されたアイテムを 40% に | 同じく、'gaps' モードのマルチドラッグ用。 |
user-select なし | ドラッグが通過したテキストを選択しないように。 |
overflow-anchor なし | 動いている行を表示し続けるためにページがスクロールしないように。 |
| ドラッグのカーソル | ポインタードラッグ中、どこでも。grabbing ですが、掴んだ要素が独自のカーソルを持つ場合は別です:リサイズハンドルの ns-resize、move、画像はドラッグ中ずっと自分のカーソルを保ちます(grab、pointer などはやはり grabbing になります)。ドラッグ可能なアイテムの cursor: grab はあなたが付けてください。別のデフォルトにするには <html> に --dnd-cursor を設定します。 |
| タッチのコールアウトなし | iOS が長押しでリンクのメニューを開かないように。 |
プロバイダーに :default-styles="false" を指定すると、これらは挿入されません。ライブラリは引き続き <html> にマークを付け、ドラッグ開始時にテキストの選択を解除し、ドラッグが終わるまで新たな選択を防ぎます。なくなるのは CSS だけです。ルールは、それを求める最初のプロバイダーによって一度だけ挿入され、残り続けます。ページのすべてのプロバイダーでオフにしてください。
<DnDProvider :default-styles="false">
Tailwind CSS
属性はバリアントとして使えます。要素自身には data-[…]、祖先には任意のバリアントを使います。
<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 にあります。Tailwind v4 のユーティリティとそれをベースにした UI キットもレイヤーにあり、そのレイヤーは後に来るので、! なしで優先されます。レイヤーに属さない CSS はいずれにせよ優先されます。Tailwind v4 では、in-data-[dnd-overlay=dragging]:shadow-xl が祖先バリアントの短い書き方です。
:global() はセレクター全体を取ります::global(html[data-dnd-dragging]) .hint は html[data-dnd-dragging] だけにコンパイルされ、ページを隠してしまいます。祖先は :global() なしで html[data-dnd-dragging] .hint と書いてください:Vue がスコープを付けるのは最後の部分だけです。UI キット
- コンポーザブルは要素だけでなくコンポーネントも受け取ります。キットのカードやリストアイテムに
useTemplateRefを使えば、ライブラリはそのルート要素を読み取ります。 - キットに対応する props があれば状態をそこに渡し(
:active="selected"、:color="isOver ? 'primary' : undefined")、残りはクラスとして渡します。 - キットのリストアイテムは、たいてい自身の背景を持たず、リストが背景を塗っています。そのため、オーバーレイ内のコピーは透けてしまいます。背景を与えてください。
[data-dnd-overlay] .v-list-item { background: rgb(var(--v-theme-surface)); } - コピーは Vuetify、Quasar、PrimeVue、Naive UI の押下リップルを除外します。そのほか運ばれるべきでないもの(ツールチップやメニューボタン)には
data-dnd-no-cloneを付けてください。
やってはいけないこと
- 並べ替え可能なアイテムの
transformやtranslateに CSS のtransitionを付けないでください。ライブラリが自分でアニメーションし、測定中はそうしたトランジションをキャンセルします。 - 並べ替え可能なアイテムを
<TransitionGroup>で囲まないでください。移動が二重にアニメーションします。リストのanimateChangesを使ってください。 - 仮想リストの行の
height、margin、paddingをアニメーションしないでください。 - ドラッグに関連して追加したトランジションは、
prefers-reduced-motion: reduceでは必ず止めてください。ライブラリは自身のものをすでに止めています。