YASD-TECH
YASD TECH
# その他

HTML Drag and Drop API の基本(dragstart・dragover・drop・dataTransfer)

投稿日:2026/8/8

更新日:2026/8/9

ttitleImage

HTML Drag and Drop API の基本(dragstart・dragover・drop・dataTransfer)

ドラッグ&ドロップをライブラリなしで実装するときの、ブラウザ標準 API の整理。React ではなく素の HTML / JavaScript としてどう動くかを先に押さえる。カンバン(カードを Todo / Doing / Done の列に移す)を例にする。


結論

  • ドラッグ&ドロップは 「つかむ → 動かす → 落とす」 の 3 段階。イベントで言うと dragstartdragoverdrop
  • dragstartdataTransfer.setData()dropgetData() これがドラッグ中のデータの受け渡し口。
  • dragoverpreventDefault() を呼ばないと drop が発火しない。 ここが一番のハマりどころ。
  • 素の DOM でも React でも考え方は同じ。違うのは「反映のしかた」だけ(DOM を動かす vs state を更新する)。
  • React なら ref / state でも動く。それでも setData() を使うのは、ブラウザや OS の境界を越えられるのが dataTransfer だけだから。

登場人物は3つ

カンバンの例
ドラッグされる要素 カード
運ばれるデータ card.id
ドロップ先 Todo / Doing / Done の列

「何を」「どのデータを持って」「どこに」落とすか、を分けて考えると迷いにくい。


全体の流れ

sequenceDiagram
    participant U as ユーザー
    participant C as カード(ドラッグ元)
    participant DT as dataTransfer
    participant Z as 列(ドロップ先)

    U->>C: つかむ
    activate C
    C->>DT: dragstart で setData し card.id を入れる
    U->>Z: 列の上へ動かす
    Z->>Z: dragover で preventDefault し落とせる場所にする
    U->>Z: 離す
    Z->>DT: drop で getData する
    DT-->>Z: card.id
    Z->>Z: アプリ側の状態を更新する
    C->>C: dragend で一時状態を片付ける
    deactivate C

ドラッグされる側

任意の要素をドラッグ可能にするには draggable="true" を付ける。

html
<div id="card-1" draggable="true">カード1</div>

そのうえで dragstart を拾う。

js
const card = document.getElementById("card-1");

card.addEventListener("dragstart", (event) => {
  event.dataTransfer.setData("text/plain", "card-1");
});

dragstartつかんだ瞬間に発火する。つまりこのタイミングで「何を運ぶのか」を決める。

テキスト選択・画像・リンクは既定でドラッグ可能なので draggable は不要。それ以外の div などは明示的に付ける必要がある。


dataTransfer は何者か

ドラッグ中だけ存在する、一時的なデータの入れ物。

flowchart LR
    A["dragstart"] -->|"setData で入れる"| B["dataTransfer"]
    B -->|"getData で読む"| C["drop"]

カード移動なら、カードオブジェクト全部ではなくまず ID だけ入れるのが分かりやすい。

js
// ドラッグ元
event.dataTransfer.setData("text/plain", "card-1");

// ドロップ先
const cardId = event.dataTransfer.getData("text/plain");

なぜ必要か

drop イベントは「どこに落とされたか」は分かる。しかしそれだけでは「何を持っていたか」が分からない。

なので dragstart の時点で card.id を入れておき、drop で取り出す。これが HTML Drag and Drop API のお作法。


なぜ setData() なのか(React なら ref でよくない?)

自アプリの中だけで完結する操作なら、ref や state にドラッグ中のアイテムを持たせる設計でも動く。それでも setData() を使う理由は、ブラウザや OS の境界を越えてデータを渡せるのが dataTransfer だけだから。

1. アプリの外に落とせる

ref / state は当然ながら自分の JavaScript 環境の中でしか有効ではない。setData() でブラウザ標準のドラッグ&ドロップに載せると、アプリの外へ出せる。

  • デスクトップや別アプリへのドロップ … Web アプリ上のテキストや要素をドラッグして、テキストエディタなどにドロップして貼り付ける
  • 他のサイト・別タブへのドロップ … 自分のアプリから、別タブで開いている他のサイトへデータを渡す
flowchart LR
    subgraph APP["自分の React アプリ"]
        R["ref / state"]
        C["カード"]
    end
    subgraph OS["ブラウザ / OS"]
        E["テキストエディタ"]
        T["別タブの他サイト"]
    end
    C -->|"setData"| DT["dataTransfer"]
    R -.->|"届かない"| OS
    DT --> E
    DT --> T

2. 複数の形式(MIME タイプ)を同時に持たせられる

1回のドラッグ操作に対して、複数の形式でデータを載せられる。

js
// 同じ要素に対して異なる形式のデータを仕込んでおく
event.dataTransfer.setData("text/plain", "https://example.com");
event.dataTransfer.setData("text/html", '<a href="https://example.com">Example</a>');

こうしておくと、ドロップ先の対応環境に応じた形式をブラウザが自動で選んで渡す。

ドロップ先 渡る形式
プレーンテキストのみ対応(テキストエディタなど) text/plainhttps://example.com
リッチテキスト対応(ワープロ・チャットツールなど) text/html のリンク付きテキスト

ref に1つの値を持たせるだけでは、この出し分けはできない。

URL を運ぶなら text/uri-list が標準の型。text/plain と併記しておくと落とせる先が広がる。

3. ドラッグ元とドロップ先を疎結合にできる

ref / state 共有だと、ドラッグ元とドロップ先が Context などで繋がっている必要があり、コンポーネントが離れているほど共有の設定が煩雑になる。

一方 drop はブラウザのネイティブイベントなので、その場で getData() すれば依存関係を持たずにデータを引ける。

使い分け

状況 選択
自アプリの画面内で完結する(カンバンの並び替えなど) ref / state でも動く
外部アプリや別タブにドロップさせたい setData() が必要
リッチテキストとプレーンテキストの両方を持たせたい setData() が必要
OS 標準のドラッグ&ドロップ挙動と連携したい setData() が必要

内部完結でも setData() に寄せておくと、後から外部連携が必要になったときに書き換えが小さく済む。


ドロップ先の作り方

ここが一番つまずく。HTML 要素は既定ではドロップ先として扱われない。

ドロップ先にしたい要素で dragover を拾って preventDefault() する。

html
<div id="drop-zone">ここに落とす</div>
js
const dropZone = document.getElementById("drop-zone");

dropZone.addEventListener("dragover", (event) => {
  event.preventDefault(); // ← これで「dropしてよい場所」になる
});

dropZone.addEventListener("drop", (event) => {
  event.preventDefault();

  const cardId = event.dataTransfer.getData("text/plain");
  console.log("drop:", cardId);
});

preventDefault() を呼ぶ2か所は役割が違う

呼ぶ場所 目的
dragover drop 先として成立させるため。これが無いと drop が発火しない
drop ブラウザの既定処理を止めて、アプリ側で処理するため

特に dragover 側が重要。


最小構成

React なしだとこれだけで動く。

html
<div id="card" draggable="true">カード</div>
<div id="drop-zone">ここに落とす</div>
js
const card = document.getElementById("card");
const dropZone = document.getElementById("drop-zone");

card.addEventListener("dragstart", (event) => {
  event.dataTransfer.setData("text/plain", "card");
});

dropZone.addEventListener("dragover", (event) => {
  event.preventDefault();
});

dropZone.addEventListener("drop", (event) => {
  event.preventDefault();

  const id = event.dataTransfer.getData("text/plain");
  dropZone.append(document.getElementById(id));
});

流れはこうなっている。

  1. card をつかむ → dragstartdataTransfer"card" を入れる
  2. drop-zone の上に移動 → dragoverpreventDefault()
  3. drop-zone の上で離す → dropdataTransfer から "card" を読む → DOM を移動する

イベント別の役割

イベント 発火タイミング 主な役割
dragstart つかんだ時 dataTransfer に運ぶデータを入れる
drag ドラッグ中(連続) 必要なら移動中の処理をする
dragenter ドロップ先候補に入った時 ハイライト開始など
dragover ドロップ先候補の上にいる間(連続) preventDefault() して drop 可能にする
dragleave ドロップ先候補から出た時 ハイライト解除など
drop 離した時 dataTransfer からデータを読んで反映する
dragend ドラッグ操作が終わった時 一時状態を片付ける

実務での基本パターン

まずはこの形で覚える。

js
// dragstart: setData する
// dragover:  preventDefault する
// drop:      preventDefault → getData → 状態を更新する
// dragend:   一時状態を片付ける

素の DOM と React の違い

React では DOM を直接動かすより、state を更新して再描画するのが自然。

js
// 素の DOM: DOM を直接動かす
dropZone.append(document.getElementById(id));
ts
// React: state を変えて再描画する
setCards((current) =>
  current.map((card) =>
    card.id === cardId ? { ...card, columnId: nextColumnId } : card,
  ),
);

考え方は同じ(drop された → 何が落ちたか知る → 落ちた場所に反映する)。違うのは反映方法だけ。

素の DOM React
反映方法 DOM を直接動かす state を変えて再描画する
属性の書き方 draggable="true" draggable(JSX の真偽値)
イベント登録 addEventListener("dragstart", ...) onDragStart / onDragOver / onDrop

ハマりどころ

drop だけ書けばよくない?

足りない。ドロップ先として成立させるには dragoverpreventDefault() が必要。「drop ハンドラを書いたのに一度も呼ばれない」の原因はほぼこれ。

dataTransfer じゃなくて state だけでもよくない?

React アプリ内で完結するなら state だけでも実装できる。ただし Web 標準としてはドラッグ中のデータ受け渡し口は dataTransfer であり、ファイルドロップ・URL ドロップ・外部アプリからのテキストドロップも同じ仕組みに乗る。詳しくは「なぜ setData() なのか」の節を参照。

dragovergetData() しても空文字が返る

dragover の時点ではデータは保護モードにあり、中身を読めない。読めるのは drop の中だけ。ドラッグ中に種類で分岐したいなら dataTransfer.types を見る。

dragleave でハイライトが点滅する

dragenter / dragleave は子要素をまたぐたびに発火する。ドロップ先の内側に子要素があると、そこに入った瞬間に親の dragleave が飛んでハイライトが消える。カウンタで入退場を数えるか、dropdragend で必ず解除する形にする。

スマホで動かない

HTML Drag and Drop API はタッチイベントに対応していない。モバイル対応が要るならポインタイベントで自前実装するか、対応ライブラリを使う。

順番を入れ替えているの?

この記事の例は順番の入れ替えではなく「所属列の変更」(card.columnId"todo" から "doing" に変える)。同じ列内の上下並び替えまでやるなら、order を持たせるか配列そのものの順序を入れ替える必要がある。


まとめ

  1. dragstartsetDatadropgetData
  2. dragoverpreventDefault() が無いと drop は来ない
  3. droppreventDefault() はブラウザ既定処理の抑止
  4. 運ぶのは ID だけにして、実体はアプリ側の状態から引く
  5. React でも API は同じ。DOM 操作の代わりに state を更新する
  6. ref / state で代用できるのは自アプリ内で完結する場合だけ。外部アプリ・別タブ・複数 MIME タイプが絡むなら setData() 一択

参考

Index

  • HTML Drag and Drop API の基本(dragstart・dragover・drop・dataTransfer)
  • 結論
  • 登場人物は3つ
  • 全体の流れ
  • ドラッグされる側
  • dataTransfer は何者か
  • なぜ必要か
  • なぜ setData() なのか(React なら ref でよくない?)
  • 1. アプリの外に落とせる
  • 2. 複数の形式(MIME タイプ)を同時に持たせられる
  • 3. ドラッグ元とドロップ先を疎結合にできる
  • 使い分け
  • ドロップ先の作り方
  • preventDefault() を呼ぶ2か所は役割が違う
  • 最小構成
  • イベント別の役割
  • 実務での基本パターン
  • 素の DOM と React の違い
  • ハマりどころ
  • drop だけ書けばよくない?
  • dataTransfer じゃなくて state だけでもよくない?
  • dragover で getData() しても空文字が返る
  • dragleave でハイライトが点滅する
  • スマホで動かない
  • 順番を入れ替えているの?
  • まとめ
  • 参考