draggable="true" 仅启用原生拖拽样式和事件,不自动移动元素;需手动监听 dragstart/dragover/drop 并用 CSS 或 transform 更新位置,移动端须降级为 touch 事件模拟。

draggable="true" 只是起点,不触发任何移动逻辑
设置 draggable="true" 仅让浏览器启用原生拖拽样式(如光标变化、半透明拖影),但**不会自动改变元素位置**。它只是触发 dragstart、dragover、drop 等事件的开关,后续所有位移必须手动计算并应用 CSS 或 DOM 操作。
常见错误是以为加了属性就“能拖着走”,结果松手后元素回到原位——因为没监听 drop,也没更新 style.left/top 或 transform。
用 dragover + drop 获取落点坐标要防默认行为
dragover 事件默认被浏览器阻止(防止打开文件等),若不调用 event.preventDefault(),drop 就永远不会触发。
在 drop 回调中获取坐标时,优先用 event.clientX/clientY(视口坐标),再减去容器偏移(getBoundingClientRect()),避免滚动条干扰:
container.addEventListener('drop', (e) => {
e.preventDefault();
const rect = container.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
draggableEl.style.left = x + 'px';
draggableEl.style.top = y + 'px';
});- 别依赖
event.pageX/pageY:在有 iframe 或缩放页面中不可靠 - 容器必须设
position: relative(或非 static),否则left/top会相对文档定位 - 如果用
transform: translate(),注意累积叠加问题,建议每次重置再设新值
移动端无法直接用原生 draggable,得换方案
iOS Safari 和多数安卓 WebView 对 draggable 支持极差,dragstart 根本不触发。此时必须放弃原生 API,改用 touchstart/touchmove/touchend 模拟。
关键差异:
-
touchmove需要e.preventDefault()阻止页面滚动(但会禁用惯性滚动,需权衡) - 触摸点坐标用
e.touches[0].clientX,不是clientX - 没有
dragover,落点判断要靠touchend时的最后坐标 - 建议统一用
transform更新位置,避免 layout 触发(比left/top性能好)
拖拽过程中的视觉反馈容易被忽略
用户需要明确知道“正在拖什么”和“能放到哪”。仅靠浏览器默认拖影不够:
- 在
dragstart中设置data-transfer或临时 class,用于高亮可放置区域(dragover时加class="drop-target") - 拖动中隐藏原元素(
draggableEl.style.opacity = 0.5),或用::after伪元素画拖影 - 松手前未进入有效区域时,
drop不执行,但应清除高亮态(监听dragleave) - 不要在
dragmove(不存在该事件!)里做计算——原生 API 没这个事件,得靠touchmove或鼠标mousemove配合标志位模拟
坐标计算、事件拦截、移动端降级、视觉连贯性——这四块漏掉任何一环,拖拽都会卡在“看起来能动,实际不听话”的状态。

















