モダンなドラッグ&ドロップライブラリのdnd-kitを試してみた
dnd-kit とは?
Reactのための、軽量、高性能、アクセス可能、拡張可能なドラッグ&ドロップツールキットです
- 機能豊富:衝突検出、ドラッグハンドル、自動スクロールなどカスタマイズ可能。
-
React対応:
useDraggable
やuseDroppable
フックが提供され、再構築不要。 - 多様な用途に対応:リストやグリッド、複数コンテナ、2Dゲームなどに対応。
- 依存なし&モジュール式:10KB未満で、外部依存がない軽量設計。
- 多入力対応:ポインタ、マウス、タッチ、キーボード入力に対応。
- カスタマイズ性:アニメーションやスタイル、衝突検出アルゴリズムを自由に設定可能。
- アクセシビリティ:キーボード操作、スクリーンリーダー対応。
- パフォーマンス重視:スムーズなアニメーションを実現。
-
プリセットあり:
dnd-kit/sortable
など、簡単に並べ替えを実装可能。
検証用の環境構築
👆今回もこちらをベースに環境構築していきます。
$ yarn create vite . --template react-ts
Current directory is not empty. Please choose how to proceed:
Remove existing files and continue
Cancel operation
❯ Ignore files and continue
.devcontainer/postAttach.sh
に以下を追加します。
yarn install
yarn dev
.devcontainer/docker-compose.yml
に以下を追加します。
services:
app:
build: .
...
working_dir: /usr/src
ports: # ports 追加
- "5173:5173"
package.json
の scripts
> dev
を以下に変更します。
"scripts": {
"dev": "vite --host=0.0.0.0",
// ...
},
ここまでして、再度VSCodeでdevcontainerを使ってコンテナを起動し直します。
ブラウザで http://localhost:5173/ にアクセスしVite + React が表示されていればOKです。
必要パッケージインストール
yarn add @dnd-kit/core
シンプルな実装
src/App.tsx
を以下に修正します。
import { DndContext, useDraggable, useDroppable } from "@dnd-kit/core";
function DraggableItem() {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: "draggable-item",
});
return (
<button
ref={setNodeRef}
style={{
transform: `translate3d(${transform?.x}px, ${transform?.y}px, 0)`,
}}
{...listeners}
{...attributes}
>
Drag me!
</button>
);
}
function DroppableArea() {
const { isOver, setNodeRef } = useDroppable({
id: "droppable-area",
});
return (
<div
ref={setNodeRef}
style={{
backgroundColor: isOver ? "lightgreen" : "lightgray",
width: 200,
height: 200,
}}
>
Drop here
</div>
);
}
function App() {
return (
<DndContext>
<DroppableArea />
<DraggableItem />
</DndContext>
);
}
export default App;
動作させてみると以下の様になります。
onDragStart / onDragEnd / onDragMove / onDragCancel / onDragOver
DndContext
には上記の onDragXXX
イベントが定義できるようになっています。
それぞれ発火するタイミングとしては以下になっています。
- onDragStart
- Drag操作開始時
- onDragEnd
- Drag操作終了時
- onDragMove
- Drag中
- onDragCancel
- esc等でDragキャンセルした時
- onDragOver
- drop対象エリアの上のHoverした時
Modifiers
Modifiersを使用すると、Sensorによって検出された移動座標を動的に変更することができるらしい。以下の様な例が挙げられる。
- 動きを1軸に制限する
- ドラッグ可能なノードコンテナの外接矩形に動きを制限する
- ドラッグ可能なノードのスクロールコンテナ境界矩形に動きを制限する
- 抵抗を加えるか、動きをクランプする
別途 @dnd-kit/modifiers
のインストールが必要になります。
yarn add @dnd-kit/modifiers
Modifiersには公式で用意しているものもあれば、カスタムする事もできます。
以下は公式で用意されているModifiersになります。
restrictToVerticalAxis
縦方向にしかDrag対象を移動できなくなる
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
function App() {
return (
<DndContext modifiers={[restrictToVerticalAxis]}>
// ...
</DndContext>
);
}
restrictToHorizontalAxis
横方向にしかDrag対象を移動できなくなる
import { restrictToHorizontalAxis } from '@dnd-kit/modifiers';
function App() {
return (
<DndContext modifiers={[restrictToHorizontalAxis]}>
// ...
</DndContext>
);
}
Sortable
名前の通り、D&Dで並び替えを行う様な場面で使います。簡単なサンプルを作成してみます。
こちらも別途 @dnd-kit/sortable
をインストールする必要があります。
yarn add @dnd-kit/sortable
新規に src/SortableItem.tsx
を以下内容で作成します。
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
export const SortableItem = (props: { id: number }) => {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: props.id });
const style = {
width: '200px',
height: '50px',
color: 'black',
backgroundColor: 'white',
border: '1px solid black',
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
{props.id}
</div>
);
};
次に src/App.tsx
を以下に修正します。
import {
DndContext,
type DragEndEvent,
KeyboardSensor,
PointerSensor,
closestCenter,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
SortableContext,
arrayMove,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { useState } from 'react';
import { SortableItem } from './SortableItem';
function App() {
const [items, setItems] = useState([1, 2, 3]);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
return (
<div style={{ width: '500px', height: '100vh' }}>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={items} strategy={verticalListSortingStrategy}>
{items.map((id) => (
<SortableItem key={id} id={id} />
))}
</SortableContext>
</DndContext>
</div>
);
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (active.id !== over?.id) {
setItems((items) => {
const oldIndex = items.indexOf(active.id as number);
const newIndex = items.indexOf(over?.id as number);
return arrayMove(items, oldIndex, newIndex);
});
}
}
}
export default App;
この状態で実行すると、以下の様にD&Dで並び替えができる様なUIが実装できます
DragOverlay
ドラッグ時に別のコンポーネントをOverlay表示してくれます。先ほどのSortableのサンプルをDrag時にOverlay表示する様にしてみます。src/App.tsx
を修正します。
// 追加
const OverlayItem = () => {
return <div style={{ width: '200px', height: '50px', background: 'red' }} />;
};
function App() {
// 追加
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
return (
<div style={{ width: '500px', height: '100vh' }}>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart} // 追加
onDragEnd={handleDragEnd}
>
...
{/* 追加 */}
<DragOverlay>
{activeId ? <OverlayItem key={activeId} /> : null}
</DragOverlay>
</DndContext>
</div>
);
// 追加
function handleDragStart(event: DragEndEvent) {
setActiveId(event.active.id);
}
}
export default App;
実行してみると以下の様にDrag時に別のコンポーネントがOverlay表示しているのが分かります。
ただ👆の例だとWindowの外にOverloayがはみ出しています。これをはみ出さないようにするModifierがあります。
restrictToWindowEdges
先ほどの DragOverlay
に restrictToWindowEdges
を設定します。
<DragOverlay modifiers={[restrictToWindowEdges]}>
👆はみ出せない様になっているのが分かるかと思います。
Discussion