拖拽效果(Pointer Events 自由拖拽回弹源码)
拖拽效果源码:Pointer Events 实现卡片自由拖拽,拖拽中放大提亮、松手弹性回弹归位,自带拖拽计数与一键重置,原生 JS 零依赖,适配鼠标与触屏。
效果演示(真实可交互)
拖拽是卡片、标签、待办列表里最直观的微交互之一。演示区有三张渐变卡片,按住任意一张即可在虚线舞台内自由拖动:拖拽过程中卡片放大、投影加深、层级抬升,实时跟随指针移动;松手后卡片带着弹性过冲回弹到初始位置,手感像拉了一根橡皮筋。舞台下方实时统计拖拽次数,点击「重置位置」可清零。实现基于 Pointer Events,一套代码同时兼容鼠标、触屏与触控笔,不需要分别写 mouse 与 touch 两套事件。拖拽逻辑与业务解耦:只要在 pointerup 时把卡片最终坐标交给自己的数据层,就能把演示变成真正的可排序列表或看板。舞台内的卡片间距与偏移已经预设好,即使只拖拽一次也能立刻感受到回弹的弹性手感。回弹归位是演示的默认行为,很多真实场景(如看板拖拽排序、文件列表重排)希望松手后落到新位置,这时只需把回弹代码替换成落位逻辑,其余手势处理(按下、跟踪、捕获、取消兜底)完全通用,可以直接复用。
按住卡片随意拖拽(已拖 0 次)
完整代码
HTML 结构:舞台是相对定位容器,三张卡片绝对定位居中,拖拽时 JS 直接改写 left/top:
<div class="dr-stage" id="drStage">
<div class="dr-card" data-hue="240">🃏 拖拽我</div>
<div class="dr-card" data-hue="170">🎯 松手回弹</div>
<div class="dr-card" data-hue="330">✨ 试试看</div>
</div>
<p>已拖 <b id="drCount">0</b> 次 <button id="drReset">重置位置</button></p>
CSS 关键点:默认开启弹性缓动过渡,拖拽中关闭过渡保证跟手:
.dr-stage { position: relative; height: 230px; overflow: hidden; touch-action: none; }
.dr-card {
position: absolute; top: 50%; left: 50%; width: 128px; height: 128px;
margin: -64px 0 0 -64px; cursor: grab; user-select: none;
transition: transform .45s cubic-bezier(.34, 1.56, .64, 1), box-shadow .45s ease;
}
.dr-card.dr-dragging { cursor: grabbing; transition: none; z-index: 2; }
原生 JS 核心逻辑:setPointerCapture 保证指针移出卡片后仍能持续跟踪:
const stage = document.getElementById('drStage');
const cards = [...stage.querySelectorAll('.dr-card')];
cards.forEach((card) => {
let startX = 0, startY = 0, originX = 0, originY = 0, dragging = false;
card.addEventListener('pointerdown', (e) => {
dragging = true;
card.setPointerCapture(e.pointerId); // 捕获指针,移出卡片不丢事件
const rect = card.getBoundingClientRect();
originX = rect.left; originY = rect.top; // 记录起始位置
startX = e.clientX; startY = e.clientY; // 记录指针起始点
card.classList.add('dr-dragging');
});
card.addEventListener('pointermove', (e) => {
if (!dragging) return;
// 起始位置 + 指针位移,换算到舞台坐标系
card.style.left = originX - stage.getBoundingClientRect().left + (e.clientX - startX) + 'px';
card.style.top = originY - stage.getBoundingClientRect().top + (e.clientY - startY) + 'px';
});
const release = () => {
if (!dragging) return;
dragging = false;
card.classList.remove('dr-dragging');
card.style.left = '50%'; card.style.top = '50%'; // 回弹到初始位置
card.style.transform = '';
};
card.addEventListener('pointerup', release);
card.addEventListener('pointercancel', release);
});
参数说明
| 参数 | 位置 | 说明 |
|---|---|---|
| 回弹缓动 | cubic-bezier(.34,1.56,.64,1) | 过冲系数制造橡皮筋回弹感,调小则更「死板」 |
| 回弹时长 | transition: transform .45s | 300-500ms 手感最佳,越短越干脆 |
| 拖拽中样式 | .dr-dragging | 关闭过渡 + 提层 + 加深投影,保证跟手 |
| 位移计算 | left/top = 起点 + 指针差 | 若改用 transform 需额外记录基点,left/top 更直观 |
| 触屏适配 | touch-action: none | 必须加在舞台容器上,否则触屏会触发页面滚动 |
兼容性与性能
- Pointer Events 在 Chrome / Firefox / Safari 13+ / Edge 均原生支持,移动端通过
touch-action: none关闭默认手势 - 拖拽中只改
left/top,单张卡片开销可忽略;如需海量元素拖拽,建议改用transform+requestAnimationFrame节流 setPointerCapture让事件不随指针移出元素而中断,无需全局监听 mousemovepointercancel兜底:系统手势打断(如来电、滚动抢手势)时安全结束拖拽,不会出现卡片「卡死」在拖拽态- 舞台容器加
overflow: hidden,卡片即使被拖到边缘也不会溢出破坏布局;拖拽计数与重置按钮用于演示状态管理,接入业务时可直接删除或替换为数据持久化逻辑
常见问题
Q: 拖拽时文字被选中? 给卡片加 user-select: none,并给舞台加 touch-action: none;前者阻止鼠标选择,后者阻止触屏长按选择与滚动。
Q: 想拖到新位置后留在那里(不回弹)? 去掉 release 里的重置代码,改为把拖拽后的 left/top 固化为新的基准点,下次拖拽从新位置开始即可。
Q: 为什么我移出卡片后卡片就不跟手了? 说明没有使用 setPointerCapture,指针一旦离开元素事件就丢失;在 pointerdown 里调用 card.setPointerCapture(e.pointerId) 即可解决。
Q: 可以商用吗? 可以。本组件为粤喵自研原创代码,免费商用无需署名。
许可与来源
- 许可证:自研原创 —— 粤喵自研原创,可免费用于个人与商业项目,无需署名。查看来源公示与许可清单
- 来源:粤喵团队原创自研(从效果思路到代码实现均为独立完成,未直接复制第三方代码)
- 首次发布:2026-09-01 · 最近更新:2026-09-01
- 修改日志:2026-09-01 首次发布;2026-09-13 补充许可证与来源标注、复制按钮说明与法务页入口
- 纠错:联系页面 / min@cactus.asia,2 个工作日内核实处理
常见问题
这个组件可以商用吗?
可以。本组件为粤喵自研原创,可免费用于个人和商业项目,无需署名。
浏览器兼容性如何?
支持所有现代浏览器(Chrome / Firefox / Safari / Edge 最新版本)。旧版 IE 不支持 CSS 动画相关特性。
怎么修改动画参数?
直接调整 CSS 中的 animation-duration(时长)、animation-timing-function(缓动曲线)等属性即可,代码中有注释说明。