tech-book-labs
ガイド(UI / インタラクション) · 2 セクション統合

dnd-kit v6 ドラッグ & ドロップ実装ガイド

dnd-kit v6 でドラッグ&ドロップ UI を組む実装パターン。sortable list の最小実装、複数カラムを跨ぐ Kanban(列移動の状態管理を含む)、useSensor / DragOverlay の細部までを 1 ページに統合した実装ノート。

著者:TechBook.net編集部 · 最終検証 2026-05-10
セクション · 2026-05-10 · @dnd-kit/sortable 10.0.0

dnd-kit v6 でソート可能なリストを作る

dnd-kit v6(@dnd-kit/core + sortable + utilities)でアクセシブルな drag & drop リストを最小コードで実装するパターン、PointerSensor / KeyboardSensor の合成、よく詰まる点を触れる demo で確認する実装メモ。

検証日: 2026-05-10

使用バージョン: @dnd-kit/core / @dnd-kit/sortable@10.0.0 / @dnd-kit/utilities

対象: React で並び替え可能なリスト・カンバン・グリッドを作りたい人

dnd-kit v6 で アクセシブル(キーボード対応)な drag & drop リスト を最小コードで組むパターン。useSortable + SortableContext + PointerSensor / KeyboardSensor の合成、よく詰まる点を動く demo で確認します。

触って試す

マウス drag で並び替え。Tab + Space で掴み、矢印キーで移動・Space で離す(キーボード操作も対応)。

なぜ dnd-kit を選ぶか

選択肢特徴
dnd-kitアクセシブル(キーボード対応)、軽量、拡張性高い
react-beautiful-dndメンテ停止傾向、a11y 強いがリスト系専用
react-dndHTML5 DnD ベース、低レベル、慣れが必要
Sortable.js + react-sortablejs命令的 API、SSR で罠が多い

a11y 重視 + sortable / kanban / grid 両対応 + activity が継続している、で dnd-kit。

drag が起こる仕組み(useSortable の内側)

useSortable({ id }) が一見シンプルなのは、内部で DndContext からの broadcast を購読して transform / transition を返しているから。

PointerSensor → DndContext → useSortable の broadcast パイプライン (クリックで拡大)

つまり useSortable は単なる subscriber。DndContext が drag state を持ち、各 sortable item は自分のターン(transform 更新)が来たら反映する、という形。

最小サンプル(ソート可能リスト)

DndContext で範囲を囲み、SortableContext に items 配列を渡し、各 item で useSortable を呼ぶ:

import {
  DndContext, closestCenter,
  PointerSensor, KeyboardSensor, useSensor, useSensors,
} from "@dnd-kit/core";
import {
  SortableContext, arrayMove, sortableKeyboardCoordinates,
  useSortable, verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";

function SortableItem({ id, children }: { id: string; children: React.ReactNode }) {
  const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
  return (
    <li
      ref={setNodeRef}
      {...attributes}
      {...listeners}
      style={{ transform: CSS.Transform.toString(transform), transition }}
    >
      {children}
    </li>
  );
}

export function SortableList() {
  const [items, setItems] = useState(["a", "b", "c"]);
  const sensors = useSensors(
    useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
    useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
  );

  return (
    <DndContext
      sensors={sensors}
      collisionDetection={closestCenter}
      onDragEnd={(e) => {
        const { active, over } = e;
        if (over && active.id !== over.id) {
          setItems((arr) => arrayMove(arr, arr.indexOf(active.id), arr.indexOf(over.id)));
        }
      }}
    >
      <SortableContext items={items} strategy={verticalListSortingStrategy}>
        <ul>
          {items.map((id) => <SortableItem key={id} id={id}>{id}</SortableItem>)}
        </ul>
      </SortableContext>
    </DndContext>
  );
}

sensor の使い分け

  • PointerSensor: マウス / タッチ統合。activationConstraint: { distance: 5 } で誤検知防止
  • KeyboardSensor: Tab で focus → Space で grab → 矢印で移動 → Space で release
  • MouseSensor / TouchSensor: 個別に分けたい時(タッチ専用 UI など)

PointerSensor + KeyboardSensor の 2 つ揃えば、a11y 要件を満たせる。

activationConstraint の決定木

distance vs delay の選択 — タッチ UI には delay 推奨 (クリックで拡大)
設定推奨用途
{ distance: 5 }デスクトップ中心、click と drag の混在 OK
{ delay: 100, tolerance: 5 }モバイル中心、長押しで drag 起動
併用は不可distance か delay のいずれか

strategy の使い分け

strategy用途
verticalListSortingStrategy縦並びリスト
horizontalListSortingStrategy横並びリスト
rectSortingStrategyグリッド
(custom)カンバン(列内 sort + 列間 move)

カンバンは「複数の SortableContext を束ねる」+ onDragOver で列間 move を捕捉、というパターン。

つまずいたポイント

  • activationConstraint を入れないとボタンクリックを drag と誤認distance: 5 または delay: 100, tolerance: 5
  • <ul> のデフォルト style:padding-left: 40px を解除しないとアイテムの transform が見た目とずれる
  • server-render + hydration 不一致:Astro / Next.js で SSR すると transform が server / client で違う。基本 client:loadclient:idle で hydrate
  • crypto.randomUUID() を id にすると key 安定性が崩れる — items が再作成される度 id が変わる。ローカル state で固定値を持つ
  • drop 後の state 反映:onDragEndarrayMove を呼ぶ。onDragOver を使うと UI 中に挿入される(用途次第)

評価

観点評価コメント
アクセシビリティキーボード / スクリーンリーダー対応
学習コスト概念(sensor / strategy / collision)を知る必要
拡張性カスタムセンサー / 衝突判定 / オーバーレイ
バンドルcore + sortable で 30KB 前後
TypeScript型推論強い

向く / 向かないケース

  • 向く: ソート可能リスト、カンバン、グリッド配置、ファイル並び替え UI
  • 向かない: 単純な「追加・削除のみ」のリスト(drag 不要)
  • 向かない: 数千要素の同時 drag(virtualization と組み合わせると複雑、用途を絞る)

関連 Topic / 関連書籍

この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:

tech-book.net /books/9784839966645

React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際

松澤 太郎 · マイナビ出版 · 2018年

「React Native」は、Facebookが開発しているスマートフォンアプリ向けの開発環境です。ほとんどのコードをJav…

詳細を tech-book.net で見る
tech-book.net /books/9784873119380

Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス

Alex Banks/Eve Porcello/宮崎 空 · オライリー・ジャパン · 2021年 · ¥3,740

Webフロントエンドの「今」を学びたい人へ! Facebookが開発したJavaScrip…

詳細を tech-book.net で見る
tech-book.net /books/9784873117881

Reactビギナーズガイド : コンポーネントベースのフロントエンド開発入門

Stoyan Stefanov/牧野 聡 · オライリー・ジャパン · 2017年 · ¥2,750

FacebookのエンジニアによるReactの入門書! ReactによるコンポーネントベースのWebフロントエンド開発の…

詳細を tech-book.net で見る
tech-book.net /books/9784297129163

TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発

手島 拓也/吉田 健人/高林 佳稀 · 技術評論社 · 2022年

新しいフロントエンドの入門書決定版! 本書はReact/Next.jsとTypeScriptを用いてWebアプリケーションを開…

詳細を tech-book.net で見る
tech-book.net /books/9784844379546

【POD】React &amp; Gatsby開発入門

竹本 雄貴
詳細を tech-book.net で見る
セクション · 2026-05-10 · @dnd-kit/sortable 10.0.0

dnd-kit v6 で kanban(列間 move + 列内 sort)

dnd-kit で Trello/Jira 風のカンバンを組む実装パターン — 複数 SortableContext を束ね、onDragOver で列間 move、onDragEnd で列内 sort、DragOverlay で表現を分離する設計を触れる demo で確認。

検証日: 2026-05-10

使用バージョン: @dnd-kit/core / @dnd-kit/sortable@10.0.0 / @dnd-kit/utilities

対象: シンプル sortable list は組めた、列を跨いだ drag が必要になった人

Trello / Jira 風の カンバン(列内 sort + 列間 move)を dnd-kit で組むパターン。複数 SortableContext を束ねる設計、onDragOver で列間 move、onDragEnd で列内 sort、DragOverlay で表現を分離する、を動く demo で確認します。

触って試す

カードを別列にドラッグできる:列間 moveonDragOver列内 sortonDragEnd で処理。掴み中のカードは DragOverlay で本体から分離して表示。

全体の責務分担

カンバン実装で一番混乱しやすい「どの event でどの state を変えるか」を 1 枚で。

カンバン drag の状態機械 — onDragOver は列間 move のみ、onDragEnd は列内 sort のみ、を厳守 (クリックで拡大)

このルールを守ると「列間移動の途中で並び順がバグる」「同じカードが 2 つ見える」といった典型バグを回避できる。

1. データ構造の選択

カンバンは「列ごとにカード配列」を持つ shape が扱いやすい:

type CardItem = { id: string; title: string };
type Board = Record<string, CardItem[]>;

const board: Board = {
  todo: [{ id: "t1", title: "..." }, { id: "t2", title: "..." }],
  doing: [{ id: "d1", title: "..." }],
  done: [{ id: "n1", title: "..." }],
};

ID は column も card も同じ namespace で衝突しない ように設計(t1, d1 のように prefix or UUID)。

2. 各列を SortableContext で囲む

列内 sort は、列ごとに独立した SortableContext を作る:

function Column({ id, cards }: { id: string; cards: CardItem[] }) {
  const { setNodeRef } = useSortable({ id, data: { type: "column" } });
  return (
    <div ref={setNodeRef}>
      <SortableContext items={cards.map((c) => c.id)} strategy={verticalListSortingStrategy}>
        {cards.map((c) => <Card key={c.id} id={c.id} title={c.title} />)}
      </SortableContext>
    </div>
  );
}

ポイント:

  • useSortable({ id }) は列自体にも付ける:空の列に drop した時、over.id がその列 ID になる
  • data: { type: "column" } で列とカードを区別しやすくする
  • SortableContextitemsその列の card ID 配列だけ を渡す

3. ヘルパー: ID から所属列を見つける

drag 操作のロジックで頻出:

function findContainer(board: Board, id: string): string | null {
  if (id in board) return id;        // 列自体の ID
  for (const col of Object.keys(board)) {
    if (board[col].some((c) => c.id === id)) return col;
  }
  return null;
}

active.id / over.id がどの列に属しているかを board から逆引き。

4. onDragOver で「列間 move」

ドラッグ中(まだドロップしていない)に列間を跨いだら、即座に board の状態を変える:

function handleDragOver(e: DragOverEvent) {
  const { active, over } = e;
  if (!over) return;
  const activeContainer = findContainer(board, String(active.id));
  const overContainer = findContainer(board, String(over.id));
  if (!activeContainer || !overContainer) return;
  if (activeContainer === overContainer) return;   // 同列内は何もしない

  setBoard((prev) => {
    const activeItems = [...prev[activeContainer]];
    const overItems = [...prev[overContainer]];
    const activeIdx = activeItems.findIndex((c) => c.id === active.id);
    const card = activeItems[activeIdx];
    activeItems.splice(activeIdx, 1);
    // 列に直接 drop なら末尾、card に drop ならその位置
    const overIdx =
      over.id === overContainer ? overItems.length : overItems.findIndex((c) => c.id === over.id);
    overItems.splice(overIdx, 0, card);
    return { ...prev, [activeContainer]: activeItems, [overContainer]: overItems };
  });
}

ポイント:

  • drop 前に board を書き換える → drop 先プレビューを “本物” にして UX が滑らか
  • 同列内は何もしない(列内 sort は dragEnd で処理)
  • 空列対応:over.id === overContainer のケースを overItems.length で扱う(末尾 push)

5. onDragEnd で「列内 sort」

ドロップ時、もう動いた後の列内で並べ替え:

function handleDragEnd(e: DragEndEvent) {
  const { active, over } = e;
  if (!over) return;
  const container = findContainer(board, String(active.id));
  if (!container) return;
  if (active.id === over.id) return;
  const overContainer = findContainer(board, String(over.id));
  if (container !== overContainer) return;   // 列間は dragOver で処理済

  setBoard((prev) => {
    const items = [...prev[container]];
    const oldIndex = items.findIndex((c) => c.id === active.id);
    const newIndex = items.findIndex((c) => c.id === over.id);
    return { ...prev, [container]: arrayMove(items, oldIndex, newIndex) };
  });
}

onDragOver で列間 move を済ませているので、ここは 同列内の最終位置決定 だけが責務。

6. DragOverlay で「掴み中」を分離表示

<DragOverlay> に「いま掴んでいる card」だけ別 component で描画、元の位置の card は半透明で残す:

import { DragOverlay } from "@dnd-kit/core";

const [activeId, setActiveId] = useState<string | null>(null);

<DndContext
  onDragStart={(e) => setActiveId(String(e.active.id))}
  onDragEnd={(e) => { setActiveId(null); /* sort */ }}
  onDragOver={...}
>
  {/* 列・カード描画 */}
  <DragOverlay>
    {activeId && (() => {
      const c = findCard(activeId);
      return c ? <Card id={c.id} title={c.title} dragging /> : null;
    })()}
  </DragOverlay>
</DndContext>

ポイント:

  • useSortabletransform は元の位置に対する差分:列間を跨ぐと数学的にずれる
  • DragOverlay は cursor 直下に絶対配置で見た目が安定
  • 元のカードは半透明(opacity: isDragging ? 0.4 : 1)で「ここから動いた」を示す

7. collisionDetection の選び方

関数用途
closestCenterデフォルト、シンプル list 向け
closestCornersカンバン推奨。列の境界が近い時、より直感的に検出
pointerWithinカーソル直下を厳密に
rectIntersection矩形の重なりベース、カードが大きい時

カンバンは closestCorners で「列の角に近い方」を検出するのが感覚的。

collision detection 4 種の選び分け — kanban は closestCorners が緑 (クリックで拡大)

つまずいたポイント

  • 同じカードを何度も間で行き来して board が破壊:onDragOver の中で「列間 move のみ」にする(同列はスキップ)
  • 空列に drop できない:列の useSortable({ id: 列ID }) を必ずセット + over.id === 列ID を空 push として扱う
  • DragOverlay 内の card が小さく見える:Overlay 内では Card のレイアウトコンテキスト(width 等)が変わる、明示 width 指定 or 元のセルを getBoundingClientRect() で測る
  • arrayMove が同列で oldIndex === newIndex を返す:if (oldIndex === newIndex) return prev で早期 return すると state 更新が不要になり re-render 削減
  • onDragOver が頻繁に発火:重い state 更新は throttle、または React 18+ の useTransition で低優先

関連 Topic / 関連書籍

この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:

tech-book.net /books/9784839966645

React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際

松澤 太郎 · マイナビ出版 · 2018年

「React Native」は、Facebookが開発しているスマートフォンアプリ向けの開発環境です。ほとんどのコードをJav…

詳細を tech-book.net で見る
tech-book.net /books/9784873119380

Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス

Alex Banks/Eve Porcello/宮崎 空 · オライリー・ジャパン · 2021年 · ¥3,740

Webフロントエンドの「今」を学びたい人へ! Facebookが開発したJavaScrip…

詳細を tech-book.net で見る
tech-book.net /books/9784873117881

Reactビギナーズガイド : コンポーネントベースのフロントエンド開発入門

Stoyan Stefanov/牧野 聡 · オライリー・ジャパン · 2017年 · ¥2,750

FacebookのエンジニアによるReactの入門書! ReactによるコンポーネントベースのWebフロントエンド開発の…

詳細を tech-book.net で見る
tech-book.net /books/9784297129163

TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発

手島 拓也/吉田 健人/高林 佳稀 · 技術評論社 · 2022年

新しいフロントエンドの入門書決定版! 本書はReact/Next.jsとTypeScriptを用いてWebアプリケーションを開…

詳細を tech-book.net で見る
tech-book.net /books/9784844379546

【POD】React &amp; Gatsby開発入門

竹本 雄貴
詳細を tech-book.net で見る

全ガイドは ガイド一覧 から。連載は 連載一覧 へ。