mcpField
W
web-dnd-dnd-kit

web-dnd-dnd-kit

github.com/agents-inc開発2645 回インストール2 日前に更新
リポジトリ

web-dnd-dnd-kit について

web-dnd-dnd-kitは、@dnd-kitでドラッグ&ドロップを実装する開発者向けのコーディングスキルです。基本から応用まで、センサー、衝突判定、オーバーレイ、アクセシビリティの設定を扱います。

主な機能

  1. 01ソート可能なリストとドロップ領域のパターン
  2. 02センサーとキーボード操作の設定
  3. 03組み合わせ可能な衝突判定アルゴリズム
  4. 04DragOverlayを使うコンテナ間移動
  5. 05スクリーンリーダー通知とドラッグハンドル

活用シーン

  1. 01ToDoやプレイリストの並べ替え
  2. 02カードを移動できるKanbanボードの構築
  3. 03ゴミ箱やカテゴリのドロップ先作成
  4. 04キーボード操作可能なドラッグ機能の追加

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/agents-inc/skills --skill web-dnd-dnd-kit

インストール後、Claude Code で/web-dnd-dnd-kitを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

metadata.yaml
reference.md
examples/
advanced.md
core.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 と同じ状態をリセットする必要があります

コメント