TypeScriptのドラッグ&ドロップ:HTML5 Drag and Drop API入門
ライブラリなしにドラッグ&ドロップを実装する最小コードです。

基本実装:要素を別の場所に移動する
<!-- index.html --> <div id="source" draggable="true">ドラッグしてね</div> <div id="target">ここにドロップ</div>
const source = document.getElementById('source')! const target = document.getElementById('target')! // ドラッグ開始:dataTransferにIDを渡す source.addEventListener('dragstart', (event: DragEvent) => { event.dataTransfer?.setData('text/plain', source.id) }) // dragover で preventDefault しないと drop が発火しない target.addEventListener('dragover', (event: DragEvent) => { event.preventDefault() }) // ドロップ:dataTransferからIDを取り出して要素を移動 target.addEventListener('drop', (event: DragEvent) => { event.preventDefault() const id = event.dataTransfer?.getData('text/plain') if (!id) return const dragged = document.getElementById(id) if (dragged) { target.appendChild(dragged) } })
イベントの種類と発火順
| イベント | 発火するタイミング | 対象 |
|---|---|---|
dragstart |
ドラッグ開始時 | ドラッグ要素 |
drag |
ドラッグ中(連続) | ドラッグ要素 |
dragenter |
ドロップ先に入った瞬間 | ドロップ先 |
dragover |
ドロップ先の上にいる間 | ドロップ先 |
dragleave |
ドロップ先から出た瞬間 | ドロップ先 |
drop |
ドロップした瞬間 | ドロップ先 |
dragend |
ドラッグ終了時 | ドラッグ要素 |
TypeScriptでの型
ドラッグイベントの型は DragEvent です。
element.addEventListener('dragstart', (event: DragEvent) => { // event.dataTransfer は DataTransfer | null event.dataTransfer?.setData('text/plain', 'value') })
event.dataTransfer は null になりうるため、オプショナルチェーン(?.)を使います。
ドラッグ中のスタイル変更
ドラッグ中の要素と、ドロップ可能なエリアにスタイルを当てると UX が上がります。
source.addEventListener('dragstart', (event: DragEvent) => { event.dataTransfer?.setData('text/plain', source.id) source.classList.add('dragging') // 透明化など }) source.addEventListener('dragend', () => { source.classList.remove('dragging') }) target.addEventListener('dragenter', (event: DragEvent) => { event.preventDefault() target.classList.add('drag-over') // 枠線を強調など }) target.addEventListener('dragleave', () => { target.classList.remove('drag-over') }) target.addEventListener('drop', (event: DragEvent) => { event.preventDefault() target.classList.remove('drag-over') // 移動処理 })
実用例:リストの並び替え
<ul id="list"> <li draggable="true" data-id="1">アイテム 1</li> <li draggable="true" data-id="2">アイテム 2</li> <li draggable="true" data-id="3">アイテム 3</li> </ul>
const list = document.getElementById('list')! let draggedItem: HTMLElement | null = null list.addEventListener('dragstart', (event: DragEvent) => { draggedItem = event.target as HTMLElement draggedItem.classList.add('dragging') }) list.addEventListener('dragend', () => { draggedItem?.classList.remove('dragging') draggedItem = null }) list.addEventListener('dragover', (event: DragEvent) => { event.preventDefault() const target = (event.target as HTMLElement).closest('li') if (!target || target === draggedItem) return // ドラッグ中のアイテムをターゲットの前後に挿入 const rect = target.getBoundingClientRect() const isAfter = event.clientY > rect.top + rect.height / 2 if (isAfter) { target.after(draggedItem!) } else { target.before(draggedItem!) } })
マウスの縦位置(event.clientY)とターゲット要素の中央を比較して、前後どちらに挿入するかを判定しています。
リスト間でアイテムを移動する
2つのリスト間でアイテムを移動する例です。
<ul id="list-a"> <li draggable="true">A-1</li> <li draggable="true">A-2</li> </ul> <ul id="list-b"> <li draggable="true">B-1</li> </ul>
let dragging: HTMLElement | null = null document.querySelectorAll('li').forEach((li) => { li.addEventListener('dragstart', () => { dragging = li }) }) document.querySelectorAll('ul').forEach((ul) => { ul.addEventListener('dragover', (event: DragEvent) => { event.preventDefault() }) ul.addEventListener('drop', (event: DragEvent) => { event.preventDefault() if (dragging) { ul.appendChild(dragging) // ドロップ先のリストに移動 } }) })
よくあるハマりポイント
drop が発火しない
dragover で event.preventDefault() を呼んでいないと drop は発火しません。必須です。
// これがないと drop が動かない target.addEventListener('dragover', (event) => { event.preventDefault() // 忘れずに })
draggable 属性が必要
ドラッグしたい要素に draggable="true" を設定しないとドラッグできません。<img> と <a> はデフォルトでドラッグ可能です。
event.target の型キャスト
event.target は EventTarget | null なので、HTMLElement にキャストして使います。
const el = event.target as HTMLElement
dragleave が子要素で発火する
ドロップ先に子要素がある場合、子要素に移動しただけでも dragleave が発火します。dragenter と dragleave のカウンターで管理するか、pointer-events: none を子要素に当てる方法があります。
let enterCount = 0 target.addEventListener('dragenter', () => { enterCount++ target.classList.add('drag-over') }) target.addEventListener('dragleave', () => { enterCount-- if (enterCount === 0) { target.classList.remove('drag-over') } }) target.addEventListener('drop', () => { enterCount = 0 target.classList.remove('drag-over') })
まとめ
| やること | コード |
|---|---|
| ドラッグ可能にする | <div draggable="true"> |
| ドラッグ開始時にデータを渡す | event.dataTransfer?.setData('text/plain', value) |
| ドロップを許可する | dragover で event.preventDefault() |
| ドロップ時にデータを受け取る | event.dataTransfer?.getData('text/plain') |
| ドラッグ中の要素を取得 | event.target as HTMLElement |
dragoverのpreventDefault()を忘れるとdropが発火しない- ドラッグ中のスタイル変更は
dragstart/dragendで classList を操作する - 並び替えには
clientYとgetBoundingClientRect()の組み合わせが定番