在日常开发中,拖拽功能无疑是一个常见的需求场景。为了更好地满足这一需求,HTML5提供了一套便捷的拖放API。这些API不仅能够帮助开发者轻松实现拖拽效果,还可以提高排查拖拽问题的效率,甚至可以让我们更加灵活地自定义拖拽场景和设计能力。
首先,我们将通过案例演示来直观地展示拖拽功能在实际应用中的表现,帮助你快速理解拖拽的基本概念和操作流程。其次,我们将详细解析拖放流程并介绍相关的API,包括拖放事件的类型、事件处理函数以及关键属性的使用方法等。最后,我们将通过案例实现和讲解来巩固所学知识,让你能够亲自动手实践,并将所学知识应用到实际项目中。
通过本文的学习,你将能够轻松掌握HTML5拖放API的使用技巧,提高拖拽功能的开发效率,为你的项目增色添彩。
一、案例演示
实现一个可拖放自定义课程表,将左侧的科目拖拽到右侧的课程表中,支持增加、修改、删除、移动功能。

二、API 介绍
拖放 API(Drag and Drop API)的本质是提供拖放过程的控制能力和事件机制,而不是直接实现 "拖放效果" 或 "放置逻辑"。它更像是一套底层工具,需要开发者结合业务逻辑自行实现完整的拖放交互。
1. 拖放过程的事件
整个拖放过程中,存在两个关键元素:拖拽元素、放置元素
拖拽元素的事件:
dragstart
:拖动开始时触发,可设置拖动携带的数据(如文本、URL、自定义数据)。drag
:拖动过程中持续触发(可用于实时更新视觉反馈)。dragend
:拖动结束时触发(无论是否成功放置)。
放置元素的事件:
dragenter
:拖动元素进入放置目标时触发(可用于高亮目标区域)。dragover
:拖动元素在放置目标上移动时触发(关键:需阻止默认行为e.preventDefault()
才能允许执行drop事件)。dragleave
:拖动元素离开放置目标时触发(可用于取消目标区域高亮)。drop
:拖动元素释放到放置目标时触发(执行实际放置逻辑,如数据添加、DOM 移动等)。

2. 设置可拖拽元素
在HTML中,文本、图片和链接是默认可以拖放的元素。其他元素都是默认不可拖动的,如果需要让其他非默认可拖动的HTML元素变得可拖动,比如<div>
、<span>
等,你需要明确地为这些元素设置 draggable="true"
属性。这样,这些元素就能够接受拖放操作了。
HTML
<div draggable="true">语文</div>
3. 设置放置元素
所有HTML元素在默认情况下都不接受拖拽元素的放置,除非通过拖拽的事件来处理来允许接受。要使一个HTML元素能够接受被拖动的元素,需要对这个元素进行一些特定的设置和事件绑定
4. DataTransfer
DataTransfer 对象是拖放操作(Drag-and-Drop)中主要用于在拖放源(Draggable Source)和放置目标(Drop Target)之间共享数据的核心机制。它就像一个 "数据中转站",负责存储拖放过程中的数据(如文本、文件、URL 等),并在拖放的各个阶段(如开始拖放、移动、放置)中传递这些数据。
除了共享数据还能设置拖放时的一些效果配置
属性 / 方法 | 作用 |
---|---|
e.dataTransfer.types | 返回拖放数据的 MIME 类型数组(如 ['text/plain', 'text/html'] ) |
setData(type, data) | 存储数据(type 为 MIME 类型,data 为字符串或 File 对象) |
getData(type) | 读取指定类型的数据(返回字符串,文件需通过 files 属性读取) |
files | 只读属性,返回拖放的文件列表(FileList 对象,仅在拖放文件时有效) |
items | 只读属性,返回 DataTransferItemList ,支持添加 / 删除多种类型数据(推荐) |
clearData([type]) | 清除指定类型的数据,若不传入类型则清除所有数据 |
effectAllowed | 设置拖放操作的允许效果(如 copy(默认) 、move 、link 等,影响鼠标样式) |
dropEffect | 设置放置时的实际效果(需与 effectAllowed配合,如最终是复制还是移动) |
三、案例实现
1.第一步:把左侧科目设置为可拖拽
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
margin: 0;
padding: 0;
font-family: sans-serif;
background: #f7f7f7;
}
.content {
display: flex;
justify-content: center;
align-items: flex-start;
gap: 40px;
padding: 40px;
max-width: 1200px;
margin: 40px auto;
}
.left {
display: flex;
flex-direction: column;
gap: 18px;
background: #fff;
padding: 24px 18px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07);
min-width: 120px;
}
div[draggable="true"] {
padding: 14px 0;
text-align: center;
border-radius: 6px;
color: #fff;
font-weight: bold;
font-size: 16px;
cursor: grab;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
user-select: none;
transition: opacity 0.2s;
}
.hover-background {
outline: 2px dashed #4f8cff;
background: #e6f0ff !important;
}
.right {
background: #fff;
padding: 24px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07);
}
table {
border-collapse: collapse;
width: 600px;
}
thead td {
background: #e9ecef;
font-weight: bold;
text-align: center;
padding: 10px 0;
border-bottom: 2px solid #d1d5db;
font-size: 16px;
}
tbody td {
width: 120px;
height: 60px;
border: 1px solid #e0e0e0;
text-align: center;
font-size: 15px;
background: #fafbfc;
transition: background 0.2s;
}
</style>
</head>
<body>
<div class="content">
<div class="left">
<div draggable="true" style="background:rgb(26, 231, 156)">语文</div>
<div draggable="true" style="background:rgb(107, 219, 15)">数学</div>
<div draggable="true" style="background:rgb(208, 133, 13)">英语</div>
<div draggable="true" style="background:rgb(30, 98, 246)">物理</div>
<div draggable="true" style="background:rgb(210, 40, 113)">化学</div>
<div draggable="true" style="background:rgb(210, 224, 26); color:#333;">生物</div>
</div>
<div class="right">
<table>
<thead>
<tr>
<td>星期一</td>
<td>星期二</td>
<td>星期三</td>
<td>星期四</td>
<td>星期五</td>
</tr>
</thead>
<tbody>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>
2.第二步:绑定拖放事件
这里采用事件委托的方式,对整个父元素进行事件绑定
javascript
// 获取父元素DOM
const container = document.querySelector('.content');
// 拖拽元素开始拖动
container.addEventListener('dragstart', e => {
});
// 拖拽元素进入放置元素
container.addEventListener('dragenter', e => {
});
// 拖拽拖在放置元素上方
container.addEventListener('dragover', e => {
});
// 拖拽元素离开放置元素
container.addEventListener('dragleave', e => {
});
// 拖拽元素拖拽结束
container.addEventListener('dragend', e => {
});
// 拖拽元素放下在放置元素
container.addEventListener('drop', e => {
});
3.第三步:编写事件处理逻辑
ondragstart:拖拽开始时,需要做以下处理
- 可以让拖拽元素原来的位置样式有些变化,这里就是整体颜色变得更透明
- 从左侧拖到右侧,为新增,鼠标显示新增的手势。右侧拖到左侧为移除,显示普通手势。(拖拽过程中默认为新增手势)
- 保存当前拖拽元素,后续操作需要用到
通过自定义属性给拖拽元素添加标识,标识此元素是新增还是移除,新增设置为 data-effect="copy"
HTML
<div class="left">
<div data-effect="copy" draggable="true" style="background:rgb(26, 231, 156)">语文</div>
<div data-effect="copy" draggable="true" style="background:rgb(107, 219, 15)">数学</div>
<div data-effect="copy" draggable="true" style="background:rgb(208, 133, 13)">英语</div>
<div data-effect="copy" draggable="true" style="background:rgb(30, 98, 246)">物理</div>
<div data-effect="copy" draggable="true" style="background:rgb(210, 40, 113)">化学</div>
<div data-effect="copy" draggable="true" style="background:rgb(210, 224, 26)">生物</div>
</div>
js
// 声明用来保存拖拽元素DOM的变量
let source = '';
// 拖拽开始
container.addEventListener('dragstart', e => {
if (!e.target.draggable) return;
// 如果是移除操作,则设置手势为移除
if (e.target.dataset.effect === "move") {
e.dataTransfer.effectAllowed = "move";
}
source = e.target;
// 设置拖拽元素的样式
source.style.opacity = '0.2'
});
ondragenter:进入放置元素,需要做以下处理
- 通过自定义属性进行标识 data-drop="copy",判断哪些元素可以接收
- 查找最近的放置节点,这样做是为了如果上面已经放置了元素,也不影响可放置元素
- 如果可以接收拖拽元素,需要设置其样式用于标识
HTML
<tbody>
<tr>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
</tr>
<tr>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
</tr>
<tr>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
</tr>
<tr>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
</tr>
</tbody>
js
// 拖拽进入
container.addEventListener('dragenter', e => {
const dropNode = getDropNode(e.target);
if (dropNode && source && dropNode.dataset.drop === source.dataset.effect) {
dropNode.classList.add('hover-background');
}
});
// 查找最近的放置节点
function getDropNode(node) {
while (node) {
if (node.dataset && node.dataset.drop) return node;
node = node.parentNode;
}
return null;
}
此时会发现,所有经过的放置元素背景色都发生了变化

ondragover:在放置元素上方,需要做以下处理
- 阻止放置元素的默认行为,避免放置失败
js
// 拖在元素上方时,阻止默认行为,避免放置失败
container.addEventListener('dragover', e => e.preventDefault());
dragleave:离开放置元素,需要做以下处理
- 查找最近的放置节点
- 清除元素背景色
js
// 拖拽离开
container.addEventListener('dragleave', e => {
const dropNode = getDropNode(e.target);
if (dropNode) dropNode.classList.remove('hover-background');
});
ondrop:松手放置时对放置元素的处理:
- 清除hover时的背景样式
- 查找最近的放置节点
- 【新增课程】清除放置元素内的内容;克隆拖拽元素;设置克隆后的元素data-effect属性只为move;恢复拖拽元素本身的样式;添加到放置元素中
- 【移除课程】将左侧区域设置自定义属性,作为为可移除区间,然后删除掉拖拽元素
HTML
<div class="left" data-drop="move">
<div data-effect="copy" draggable="true" style="background:rgb(26, 231, 156)">语文</div>
<div data-effect="copy" draggable="true" style="background:rgb(107, 219, 15)">数学</div>
<div data-effect="copy" draggable="true" style="background:rgb(208, 133, 13)">英语</div>
<div data-effect="copy" draggable="true" style="background:rgb(30, 98, 246)">物理</div>
<div data-effect="copy" draggable="true" style="background:rgb(210, 40, 113)">化学</div>
<div data-effect="copy" draggable="true" style="background:rgb(210, 224, 26)">生物</div>
</div>
</div>
js
// 放下
container.addEventListener('drop', e => {
// 清除hover时的样式
clearHoverClass();
// 获取最近的放置节点
const dropNode = getDropNode(e.target);
if (dropNode && source && dropNode.dataset.drop === source.dataset.effect) {
// 如果是新增课程
if (dropNode.dataset.drop === "copy") {
dropNode.innerHTML = "";
source.style.opacity = "1";
const cloned = source.cloneNode(true);
cloned.dataset.effect = "move";
dropNode.appendChild(cloned);
} else {
// 移除课程
source.remove();
}
}
});
function clearHoverClass() {
document.querySelectorAll('.hover-background').forEach(ele => ele.classList.remove("hover-background"));
}
dragend:拖拽元素结束拖拽,把保存DOM的变量变为null
js
// 拖拽结束
container.addEventListener('dragend', e => {
if (source) source = null;
});
四、参考代码
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>课程表拖拽</title>
<style>
body {
margin: 0;
padding: 0;
font-family: sans-serif;
background: #f7f7f7;
}
.content {
display: flex;
justify-content: center;
align-items: flex-start;
gap: 40px;
padding: 40px;
max-width: 1200px;
margin: 40px auto;
}
.left {
display: flex;
flex-direction: column;
gap: 18px;
background: #fff;
padding: 24px 18px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07);
min-width: 120px;
}
div[draggable="true"] {
padding: 14px 0;
text-align: center;
border-radius: 6px;
color: #fff;
font-weight: bold;
font-size: 16px;
cursor: grab;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
user-select: none;
transition: opacity 0.2s;
}
.hover-background {
outline: 2px dashed #4f8cff;
background: #e6f0ff !important;
}
.right {
background: #fff;
padding: 24px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07);
}
table {
border-collapse: collapse;
width: 600px;
}
thead td {
background: #e9ecef;
font-weight: bold;
text-align: center;
padding: 10px 0;
border-bottom: 2px solid #d1d5db;
font-size: 16px;
}
tbody td {
width: 120px;
height: 60px;
border: 1px solid #e0e0e0;
text-align: center;
font-size: 15px;
background: #fafbfc;
transition: background 0.2s;
}
</style>
</head>
<body>
<div class="content">
<div class="left" data-drop="move">
<div data-effect="copy" draggable="true" style="background:rgb(26, 231, 156)">语文</div>
<div data-effect="copy" draggable="true" style="background:rgb(107, 219, 15)">数学</div>
<div data-effect="copy" draggable="true" style="background:rgb(208, 133, 13)">英语</div>
<div data-effect="copy" draggable="true" style="background:rgb(30, 98, 246)">物理</div>
<div data-effect="copy" draggable="true" style="background:rgb(210, 40, 113)">化学</div>
<div data-effect="copy" draggable="true" style="background:rgb(210, 224, 26)">生物</div>
</div>
<div class="right">
<table>
<thead>
<tr>
<td>星期一</td>
<td>星期二</td>
<td>星期三</td>
<td>星期四</td>
<td>星期五</td>
</tr>
</thead>
<tbody>
<tr>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
</tr>
<tr>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
</tr>
<tr>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
</tr>
<tr>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
<script>
const container = document.querySelector('.content');
let source = null;
// 拖在元素上方时,阻止默认行为,避免放置失败
container.addEventListener('dragover', e => e.preventDefault());
// 拖拽开始
container.addEventListener('dragstart', e => {
if (!e.target.draggable) return;
// 如果是移除操作,则设置手势为移除
if (e.target.dataset.effect === "move") {
e.dataTransfer.effectAllowed = "move";
}
source = e.target;
// 设置拖拽元素的样式
e.target.style.opacity = '0.2'
});
// 拖拽进入
container.addEventListener('dragenter', e => {
const dropNode = getDropNode(e.target);
if (dropNode && source && dropNode.dataset.drop === source.dataset.effect) {
dropNode.classList.add('hover-background');
}
});
// 拖拽离开
container.addEventListener('dragleave', e => {
const dropNode = getDropNode(e.target);
if (dropNode) dropNode.classList.remove('hover-background');
});
// 拖拽结束
container.addEventListener('dragend', e => {
if (source) source = null;
});
// 放下
container.addEventListener('drop', e => {
// 清除hover时的样式
clearHoverClass();
// 获取最近的放置节点
const dropNode = getDropNode(e.target);
if (dropNode && source && dropNode.dataset.drop === source.dataset.effect) {
// 如果是新增课程
if (dropNode.dataset.drop === "copy") {
dropNode.innerHTML = "";
source.style.opacity = "1";
const cloned = source.cloneNode(true);
cloned.dataset.effect = "move";
dropNode.appendChild(cloned);
} else {
// 移除课程
source.remove();
}
}
});
// 查找最近的放置节点
function getDropNode(node) {
while (node) {
if (node.dataset && node.dataset.drop) return node;
node = node.parentNode;
}
return null;
}
function clearHoverClass() {
document.querySelectorAll('.hover-background').forEach(ele => ele.classList.remove("hover-background"));
}
</script>
</html