web-dnd-dnd-kit について
web-dnd-dnd-kitは、@dnd-kitでドラッグ&ドロップを実装する開発者向けのコーディングスキルです。基本から応用まで、センサー、衝突判定、オーバーレイ、アクセシビリティの設定を扱います。
主な機能
- 01ソート可能なリストとドロップ領域のパターン
- 02センサーとキーボード操作の設定
- 03組み合わせ可能な衝突判定アルゴリズム
- 04DragOverlayを使うコンテナ間移動
- 05スクリーンリーダー通知とドラッグハンドル
活用シーン
- 01ToDoやプレイリストの並べ替え
- 02カードを移動できるKanbanボードの構築
- 03ゴミ箱やカテゴリのドロップ先作成
- 04キーボード操作可能なドラッグ機能の追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-dnd-dnd-kitインストール後、Claude Code で/web-dnd-dnd-kitを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
metadata.yamlreference.mdexamples/advanced.mdcore.md詳細
@dnd-kit のドラッグ&ドロップパターン
クイックガイド:
@dnd-kit/coreは基本機能であるDndContext、useDraggable、useDroppableを提供し、@dnd-kit/sortableは並べ替え可能なリスト向けにuseSortable、SortableContext、arrayMoveを追加します。ドラッグ中に DOM 上の要素が並べ替えられることはありません。要素は CSS の transform で移動し、ドロップ時に状態が更新されます。入力はセンサーを通じて受け取ります。センサーは独立したプラグインのため、キーボード対応は標準で備わっている動作ではなく、追加するセンサーです。衝突検出はプラグインで差し替えられ、どれを選ぶかはレイアウトによって異なります。ドラッグ中にドラッグ対象の要素がクリップされる、またはアンマウントされる場合は、DragOverlayが必要です。
詳細リソース:
- examples/core.md — ドラッグ可能・ドロップ可能なコンポーネント、ソート可能なリスト、センサーの設定、衝突検出の組み合わせ、アナウンス、ドラッグハンドル
- examples/advanced.md — ソート可能な要素と組み合わせた DragOverlay、複数コンテナーのカンバン、修飾子、カスタム衝突検出、無効化されたアイテム、アイテムのメタデータ
- reference.md — フックのシグネチャと戻り値、イベントハンドラーの型、ソート戦略、衝突検出アルゴリズム、修飾子、デフォルトのキー割り当て、適用される ARIA 属性
該当する方法
- 単一のリストで、アイテムがその場にとどまる場合。 単一の
SortableContext、closestCenter、アイテム自体への transform を使用します。最もシンプルで、追加でマウント状態を維持するものはありません。examples/core.md を参照してください。 - アイテムがコンテナー間を移動する場合、またはリストがスクロールする場合。 どちらの場合も、ドラッグ中にドラッグ対象の要素がアンマウントされるかクリップされるため、状態に
DragOverlayとactiveIdが必要です。複数コンテナーの場合は、コンテナー間の移動にclosestCornersとonDragOverハンドラーも使用します。examples/advanced.md を参照してください。 - 並べ替えではなくドロップゾーンを使う場合 — ゴミ箱、アップロード先、カテゴリ別のビンなど。
@dnd-kit/coreのみを使用し、ソート用パッケージは不要です。通常はpointerWithinを使います。examples/core.md のパターン 1 と 4 を参照してください。
<critical_requirements>
@dnd-kit のコードを書く前に
すべての参加要素を 1 つの <DndContext> でラップしてください。 フックはコンテキストからセンサー、衝突状態、アクティブなドラッグを読み取ります。コンテキストの外では例外をスローせず、何もしない値を返すため、プロバイダーがないと何も起きていないように見えます。
KeyboardSensor を追加し、リストがソート可能な場合は sortableKeyboardCoordinates を使ってください。 センサーはオプトイン方式なので、ポインター入力のみで設定されたコンテキストは、キーボードでは一切操作できません。座標ゲッターを使うと、矢印キーで固定ピクセル単位ではなく、項目間を移動できます。
PointerSensor にアクティベーション制約を設定してください — distance、またはタッチ操作の場合は tolerance を指定した delay を設定します。設定しないと、クリックのたびにドラッグが始まり、リンクやボタンでもある項目がクリックできなくなります。
DragOverlay はマウントしたままにし、子要素を条件付きでレンダリングしてください。 ドロップアニメーションは、オーバーレイが子要素をアンマウントするときに再生されます。オーバーレイ自体をアンマウントすると、アニメーションの対象がなくなります。
ドロップハンドラーから新しい配列を返してください。 arrayMove は純粋関数で、並べ替えたコピーを返します。状態には触れず、既存の配列をその場で変更しても、React が再レンダリングするための変更が残りません。
</critical_requirements>
自動検出: @dnd-kit/core、@dnd-kit/sortable、@dnd-kit/utilities、@dnd-kit/modifiers、DndContext、useDraggable、useDroppable、useSortable、SortableContext、DragOverlay、useSensor、useSensors、PointerSensor、KeyboardSensor、TouchSensor、closestCenter、closestCorners、rectIntersection、pointerWithin、arrayMove、sortableKeyboardCoordinates、verticalListSortingStrategy、setActivatorNodeRef、CSS.Transform、restrictToVerticalAxis
対象:
- ソート可能なリスト — 並べ替え可能な ToDo、プレイリスト、ナビゲーション、フォーム項目の順序
- カンバンボードや、項目をコンテナー間で移動するあらゆるレイアウト
- ドロップゾーン:ゴミ箱、カテゴリー用のボックス、ページ内のアップロード先
- 項目のどの部分からドラッグを始めるかを制限するドラッグハンドル
- キーボードで操作でき、スクリーンリーダーに通知されるドラッグ操作
- 移動を軸、親要素、ビューポート内に制限すること
ここでは扱わないもの:
- ドラッグ操作を伴わない並べ替え — 上へ移動/下へ移動のコントロールは、同じ配列操作を行う通常のボタンです
- OS からドラッグしてきたファイル。これはブラウザー独自のドラッグ&ドロップであり、センサーを経由しません
- アニメーションがドロップ先よりも重要となる、物理演算やジェスチャーによる動き
- ドラッグ中やホバー中の項目の見た目 — ここにあるコンポーネントはすべて
styleとclassNameを受け取り、見た目はそれを管理する側が決めます - 新しい順序の永続化 — ドロップハンドラーは配列を生成しますが、その配列の書き込み先はこのスキルの対象外です
ドラッグ中に DOM 内の要素が移動することはありません。 項目は CSS transform で移動し、配列はドロップ時に一度だけ並べ替えられます。そのため、SortableContext の items には、レンダリングされた子要素と同じ ID を同じ順序で並べる必要があります。ストラテジーは配列内のインデックスから各項目の移動量を計算するため、一致していないとオフセットが誤って計算されます。
入力は動作ではなく、プラグインです。 センサーが設定されていない DndContext は何にも反応しません。そのため、キーボードサポートは自動で付いてくるものではなく、意図的に追加するものとなります。これは最もよく追加し忘れられるものです。
アクセシビリティは組み込まれていますが、自動ではありません。 useDraggable は role、aria-roledescription、tabindex、aria-describedby を自動的に適用します。一方、アナウンスではデフォルトで項目の ID が使われるため、スクリーンリーダーの利用者には何も伝わりません。位置を説明する announcements を指定する必要があります。
<decision_framework>
パッケージの選び方
Reorderable lists -> @dnd-kit/core + @dnd-kit/sortable + @dnd-kit/utilities
Drop zones only -> @dnd-kit/core
Constrained movement -> add @dnd-kit/modifiers
Transform と DragOverlay の使い分け
Items move between containers? -> DragOverlay (source unmounts mid-drag)
Draggable inside a scrolling or
virtualized container? -> DragOverlay (transform is clipped by overflow)
Preview should differ from the item? -> DragOverlay
None of these -> transform on the item; simpler, less state
衝突アルゴリズムの選び方
Single sortable list -> closestCenter forgiving, needs no overlap
Stacked containers (Kanban) -> closestCorners resolves nested targets
Precise zones (trash, bins) -> pointerWithin pointer must be inside
General drop targets -> rectIntersection the default
pointerWithinは、キーボードでドラッグしている間は判定に使えるポインターがないため、何も返しません。単独で使うのではなく、フォールバックとしてclosestCenterと組み合わせてください。
ソート戦略、モディファイア、アルゴリズムの全一覧はreference.mdにあります。
</decision_framework>
基本パターン
パターン 1:基本的なドラッグ&ドロップ
DndContextで2つのフックを連携させます。useDraggableは要素にリスナーとARIA属性を設定し、useDroppableはドロップ先を登録して、要素がその上にあるかを通知します。
function Draggable({ id, children }: DraggableProps) {
const { attributes, listeners, setNodeRef, transform } = useDraggable({ id });
return (
<div
ref={setNodeRef}
style={{ transform: CSS.Transform.toString(transform) }}
{...listeners}
{...attributes}
>
{children}
</div>
);
}
<DndContext
onDragEnd={({ over }) => setDroppedIn(over ? String(over.id) : null)}
>
<Draggable id="item-1">Drag me</Draggable>
<Droppable id="zone-a">Drop zone</Droppable>
</DndContext>;
ドラッグがすべてのドロップ先の外で終了すると、overはnullになり、これはキャンセルを意味します。
全コード:examples/core.md
パターン 2:ソート可能なリスト
useSortableはuseDraggableとuseDroppableを組み合わせたものなので、IDは両方で一意である必要があります。ドロップ時にはarrayMoveで並べ替え後の配列を作成します。
function handleDragEnd({ active, over }: DragEndEvent) {
if (!over || active.id === over.id) return;
setItems((prev) => {
const oldIndex = prev.findIndex((i) => i.id === active.id);
const newIndex = prev.findIndex((i) => i.id === over.id);
return arrayMove(prev, oldIndex, newIndex);
});
}
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext
items={items.map((i) => i.id)} // same ids, same order as the children below
strategy={verticalListSortingStrategy}
>
{items.map((item) => (
<SortableItem key={item.id} id={item.id}>
{item.label}
</SortableItem>
))}
</SortableContext>
</DndContext>;
全コード:examples/core.md
パターン 3:センサーとアクティベーション制約
センサーはドラッグの開始方法を決定します。useSensorsで組み合わせてください。
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: ACTIVATION_DISTANCE_PX }, // click stays a click
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates, // arrow keys step item to item
}),
);
<DndContext sensors={sensors}>{/* ... */}</DndContext>;
異なる制約が必要な場合は、PointerSensorの代わりにMouseSensorとTouchSensorを個別に使います。タッチ操作では通常、delayとtoleranceを設定し、スクロール操作がドラッグと判定されないようにします。
全コード:examples/core.md
パターン 4:衝突判定
組み込みアルゴリズムを1つ渡すか、ドロップ先ごとに異なる動作が必要な場合は、複数のアルゴリズムを関数にまとめてください。
const composedCollision: CollisionDetection = (args) => {
const pointerCollisions = pointerWithin(args);
if (pointerCollisions.length > 0) return pointerCollisions;
return closestCenter(args); // fallback: pointerWithin returns nothing for keyboard drags
};
<DndContext collisionDetection={composedCollision}>{/* ... */}</DndContext>;
委譲する前にargs.droppableContainersをフィルタリングすると、ゴミ箱領域には一方のアルゴリズムを、その周囲のリストには別のアルゴリズムを適用できます。
全コード:examples/core.md
パターン 5:DragOverlay
オーバーレイはドラッグのプレビューを独自のレイヤーに描画するため、リストのオーバーフローの影響を受けず、元のアイテムがまだ存在しているかどうかにも左右されません。
<DndContext
onDragStart={({ active }) => setActiveId(String(active.id))}
onDragEnd={(event) => {
handleDragEnd(event);
setActiveId(null);
}}
onDragCancel={() => setActiveId(null)} // Escape ends the drag too
>
{/* containers and sortable items */}
<DragOverlay>
{activeItem ? <ItemPreview item={activeItem} /> : null}
</DragOverlay>
</DndContext>
子要素は表示専用です。オーバーレイ内でuseDraggableを呼び出すと、すでにドラッグ中のアイテムが2つ目のドラッグ可能要素として登録されます。
全コード:examples/advanced.md
パターン 6:複数コンテナーのソート(カンバン)
各列はドロップ先であり、自身のSortableContextも内包します。アイテムが境界を越えたときにonDragOverで移動し、onDragEndで列内の順序を確定します。
function handleDragOver({ active, over }: DragOverEvent) {
if (!over) return;
const from = findContainer(active.id);
const to = findContainer(over.id);
if (!from || !to || from === to) return; // same column: onDragEnd handles it
setColumns((prev) => moveBetweenColumns(prev, active.id, over.id, from, to));
}
<DndContext
collisionDetection={closestCorners}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
{Object.entries(columns).map(([id, items]) => (
<KanbanColumn key={id} id={id} items={items} />
))}
<DragOverlay>
{activeItem ? <KanbanCard item={activeItem} /> : null}
</DragOverlay>
</DndContext>;
over.idには衝突判定の結果に応じて列IDまたはアイテムIDが入るため、findContainerはどちらにも対応する必要があります。
全コード:examples/advanced.md
パターン 7:キーボード操作とスクリーンリーダーのアナウンス
組み込みのアナウンスではアイテムをIDで示します。スクリーンリーダーのユーザーが移動を把握するために必要な位置情報に置き換えてください。
function createAnnouncements(items: string[]) {
const at = (id: UniqueIdentifier) =>
`position ${items.indexOf(String(id)) + 1} of ${items.length}`;
return {
onDragStart: ({ active }) => `Picked up item at ${at(active.id)}`,
onDragOver: ({ over }) =>
over ? `Moved to ${at(over.id)}` : "No longer over a drop target",
onDragEnd: ({ over }) =>
over ? `Dropped at ${at(over.id)}` : "Dropped outside a valid target",
onDragCancel: ({ active }) => `Cancelled. Returned to ${at(active.id)}`,
};
}
<DndContext announcements={createAnnouncements(itemIds)}>
{/* ... */}
</DndContext>;
screenReaderInstructionsはもう一方を担います。ドラッグ可能な要素にフォーカスしたときに読み上げられる説明を設定できます。既定値は英語です。
全コード:examples/core.md
パターン 8:ドラッグハンドル
setActivatorNodeRefは、移動する要素とは別に、ドラッグを開始する要素を指定します。
<div ref={setNodeRef} style={style}>
<button
ref={setActivatorNodeRef}
{...listeners}
{...attributes}
aria-label={`Reorder ${label}`}
>
☰
</button>
{children}
</div>
リスナーと属性はコンテナーではなくハンドルに設定します。これにより、アイテムの残りの部分はクリックや選択が可能になります。
全コード:examples/core.md
パターン 9:モディファイア
モディファイアは、ドラッグ位置を適用する前に変換します。DndContextとDragOverlayには、それぞれ独立して設定できます。
<DndContext modifiers={[restrictToParentElement]}>
<DragOverlay modifiers={[restrictToWindowEdges]}>
{activeItem ? <ItemPreview item={activeItem} /> : null}
</DragOverlay>
</DndContext>
縦方向のリストでrestrictToVerticalAxisを使うと、並べ替えの精度を損なう横方向のブレをなくせます。
全コード:examples/advanced.md
<red_flags>
注意すべき問題
実行時に問題が発生します:
DragOverlayの子要素でのuseDraggable— すでにドラッグ中の項目に対して、2つ目のドラッグ可能要素を登録しますSortableContextのitemsが、レンダリングされた子要素と ID または順序で一致しない — 配置換えはその配列内のインデックスに基づいて計算されるため、アニメーションとドロップ位置が正しくなくなりますuseSortableが別のドラッグ可能要素またはドロップ可能要素と同じ ID を共有している — 両方として登録されるため、ID は両方のセットで一意でなければなりません。一方、通常のuseDraggableとuseDroppableは別々のストアに登録されるため、同じ ID を共有できますpointerWithinのみを使用する — キーボードでのドラッグにはポインターがないため、ターゲットが解決されず、ドロップしても何も起こりません
予想外の動作:
- 積み重なったコンテナーでの
closestCenterは、その中のカードではなく列を解決します。closestCornersは4つの角すべてを測定し、入れ子になったターゲットを解決します rectSortingStrategyはデフォルトですが、仮想化をサポートしません。垂直および水平のリスト戦略はサポートしますCSS.Transform.toString()にはscaleX/scaleYが含まれます。CSS.Translate.toString()は位置のみを指定する形式で、通常、リストではこちらを使いますCSS.Transform.toString(null)はundefinedを返すため、style.transformに安全に割り当てられますDragOverlayはデフォルトではポータル化されないため、祖先要素のoverflowとスタッキングコンテキストの影響を受けます。これらを回避するには、createPortalでラップしますactive.data.currentとover.data.currentには、dataとして渡された値が格納されます。これにより、ハンドラーはカードや列を検索せずに見分けられます- スクリーンリーダー向けの指示とアナウンスはデフォルトで英語であり、翻訳ではなく置き換える必要があります
- デフォルトのキー操作は、Space または Enter でドラッグの開始とドロップ、矢印キーで移動、Escape でキャンセルです。Escape の経路は
onDragCancelであり、onDragEndと同じ状態をリセットする必要があります