レビュー済み·難易度: 中級·更新: 2026-04-18

HTML5 Drag & Drop API でリストの順序を並べ替える

外部ライブラリを使わず HTML5 の Drag & Drop API(dragstart / dragover / drop イベント)でリストアイテムの順序をドラッグで並べ替える実装例。

nextjsui-component

対応バージョン

nextjs 15react 19

前提環境

React の useState と useRef の基本を理解していること

概要

HTML5 の Drag & Drop API(dragstart / dragover / drop イベント)を使い、外部ライブラリなしでリストアイテムをドラッグで並べ替える。dragIndexhoverIndexuseRef で管理して順序を入れ替えるパターンを示す。

インストール

# 追加インストールは不要

実装

ドラッグ可能リストコンポーネント

// components/DraggableList.tsx
"use client";

import { useRef, useState } from "react";

type Item = { id: number; label: string };

type Props = {
  initialItems: Item[];
};

export function DraggableList({ initialItems }: Props) {
  const [items, setItems] = useState<Item[]>(initialItems);
  const dragIndex = useRef<number | null>(null);
  const [draggingId, setDraggingId] = useState<number | null>(null);
  const [hoverIndex, setHoverIndex] = useState<number | null>(null);

  function handleDragStart(index: number, id: number) {
    dragIndex.current = index;
    setDraggingId(id);
  }

  function handleDragOver(e: React.DragEvent, index: number) {
    e.preventDefault(); // drop を有効にするために必要
    setHoverIndex(index);
  }

  function handleDrop(dropIndex: number) {
    const from = dragIndex.current;
    if (from === null || from === dropIndex) {
      cleanup();
      return;
    }

    setItems((prev) => {
      const next = [...prev];
      const [moved] = next.splice(from, 1);
      next.splice(dropIndex, 0, moved);
      return next;
    });
    cleanup();
  }

  function cleanup() {
    dragIndex.current = null;
    setDraggingId(null);
    setHoverIndex(null);
  }

  return (
    <ul className="w-full max-w-sm space-y-2">
      {items.map((item, index) => (
        <li
          key={item.id}
          draggable
          onDragStart={() => handleDragStart(index, item.id)}
          onDragOver={(e) => handleDragOver(e, index)}
          onDrop={() => handleDrop(index)}
          onDragEnd={cleanup}
          className={`flex cursor-grab items-center gap-3 rounded-lg border px-4 py-3 text-sm
            transition-all select-none
            ${draggingId === item.id ? "opacity-40" : ""}
            ${hoverIndex === index && draggingId !== item.id
              ? "border-blue-400 bg-blue-50"
              : "border-gray-200 bg-white"
            }
          `}
        >
          <span className="text-gray-400">⠿</span>
          <span className="text-gray-800">{item.label}</span>
        </li>
      ))}
    </ul>
  );
}

使用例

// app/page.tsx
import { DraggableList } from "@/components/DraggableList";

const initialItems = [
  { id: 1, label: "タスク A" },
  { id: 2, label: "タスク B" },
  { id: 3, label: "タスク C" },
  { id: 4, label: "タスク D" },
  { id: 5, label: "タスク E" },
];

export default function Page() {
  return (
    <main className="flex min-h-screen flex-col items-center p-8">
      <h1 className="mb-6 text-2xl font-bold">ドラッグで並べ替え</h1>
      <DraggableList initialItems={initialItems} />
    </main>
  );
}

ポイント

  • draggable 属性を要素に付けることで HTML5 DnD が有効になる。dragstart でドラッグ開始インデックスを記録する
  • dragovere.preventDefault() を呼ばないと drop イベントが発火しない(重要)
  • インデックスの入れ替えは splice(from, 1) で取り出し、splice(dropIndex, 0, moved) で挿入する方式
  • useRef でドラッグ中インデックスを管理することで、state 更新なしにドラッグ中の位置を追跡できる(再レンダリングを抑える)
  • 本番環境で複雑な DnD(グリッド間移動・タッチ対応等)が必要な場合は @dnd-kit/core(アクセシビリティ対応あり)や react-beautiful-dnd を検討する

注意点

外部ライブラリなしで実装することで DnD の仕組みを理解できる。本番環境で複雑な DnD が必要な場合は @dnd-kit/core や react-beautiful-dnd を検討する。

関連サンプル