
6/7/2026 · 8:11
UI 动效 012:Drag to Reorder,为什么拖动时列表会自己让位
这期拆解 Drag to Reorder / 拖拽排序:它适合待办、播放队列和设置项排序,关键是让被拖项目抬起、用中心线触发邻项让位,再在释放后吸附到新槽位。文中用自制 GIF、Material Design、NN/g、Pointer Events 和 Motion 示例说明怎么做得顺,也怎么避免触屏和无障碍坑。
把列表里的第二项按住往下拖时,好动效不会让卡片瞬移。被拖的那一项先浮起来,下面的项目提前让出一个空位,手指松开后再吸回新的槽位。Material Design 3 把这种手势归到 Drag / Pick up and move:列表可以通过拖动项目重排,长按加拖动也常用于移动内容。1

这叫什么
这个动效通常叫 Drag to Reorder,中文可以写作「拖拽排序」或「拖动重排」。它常见于待办清单、播放队列、收藏分组、编辑模式下的设置项排序。NN/g 把 drag-and-drop 描述为一种直接操作,典型步骤是先选中对象,再移动指针,最后释放完成 drop;它特别适合分组、重排、移动或调整对象。2
它不适合藏得太深。拖拽需要用户知道「这里能抓」,还要控制较精细的位置;NN/g 也提醒,drag-and-drop 可能低效、不精确,尤其在长距离移动或触屏上更容易出错。2 所以在移动端的设置页里,最好同时给出明确的拖拽把手、编辑态,或者提供「上移 / 下移」这类替代操作。
它为什么看起来像列表会自己让位
这个动效不是把 DOM 顺序一改就结束。真正让它顺的,是系统在用户松手前就给出「将会落到哪里」的预览。
- 先把项目抬起来。 被拖动的卡片会加阴影、描边或轻微放大,让它看起来离开了列表平面。NN/g 建议在对象被抓住后给出清晰反馈,例如轮廓、对比色、阴影、偏移或 ghost preview。2
- 再判断是否越过邻项。 一个好用的触发点通常不是卡片边缘,也不是鼠标点,而是被拖项目的中心线。NN/g 认为,当被拖项目的中心越过下方项目边缘时再触发重排,手感会比「边缘一碰就动」或「鼠标点碰到才动」更自然。2
- 邻项用短动画让位。 用户还没松手,背景项目就先移动到新位置,等于提前展示 drop 的结果。NN/g 建议列表重排时让背景对象在释放前移动出去,并用大约 100ms 的快速动画和 easing 来保持自然感。2
- 松手后吸附到槽位。 最后一步不是自由停在任意坐标,而是提交新的顺序,再把卡片吸到目标槽位。Motion 的
Reorder.Group通过values和onReorder管理列表顺序,Reorder.Item已配置布局动画,新项目增删或顺序变化时,周围项目会自动动画到新位置。3
实现时抓住三根线
第一根线是输入。Web 上不要分开写 mouse、touch、pen 三套逻辑,Pointer Events 本来就是为了用一套 DOM 事件模型处理鼠标、触控和笔输入。4 开始拖动后,可以调用
setPointerCapture(),让后续 pointer 事件继续派发给当前元素,即使指针已经移出元素边界也不丢事件。4第二根线是排序判断。每次
pointermove 都可以拿当前拖动卡片的中心点,和其它卡片的边界比较。中心越过谁,就把目标索引改到谁的前后。这个判断比「鼠标点在哪」更稳,因为用户经常不是从卡片正中间按下去。第三根线是布局动画。Josh W. Comeau 在讲列表重排时指出,列表顺序变化后,元素的 CSS 属性本身可能没变,单靠普通 CSS transition 无法自动知道「它从哪里移动到哪里」。解决办法是记录旧位置和新位置,算出差值,再用 FLIP:First、Last、Invert、Play。5 实际播放时优先动
transform,因为 transform 可以做 translate、scale、rotate 等视觉变换;CSS transition 则负责定义元素在两个状态之间怎么过渡。67let dragging = null;
function onPointerDown(event, item) {
dragging = {
id: item.id,
startY: event.clientY,
currentY: event.clientY,
fromIndex: item.index,
};
event.currentTarget.setPointerCapture(event.pointerId);
}
function onPointerMove(event) {
if (!dragging) return;
dragging.currentY = event.clientY;
const draggedCenter = getDraggedCenter(dragging);
const nextIndex = findIndexByCenter(draggedCenter);
if (nextIndex !== dragging.fromIndex) {
reorderPreview(dragging.id, nextIndex); // 只更新预览顺序
animateNeighborsWithTransform(); // 邻项让位
}
}
function onPointerUp() {
commitOrder(); // 写入真实顺序
snapDraggedItem(); // 吸附回目标槽位
dragging = null;
}如果用 Motion,就不必自己手写完整 FLIP。最小结构是
Reorder.Group 包住列表,把数组传给 values,再用 onReorder 更新状态;每个 Reorder.Item 传入自己的 value。需要只允许拖把手触发时,可以关闭默认拖动监听,再用 useDragControls 从把手的 onPointerDown 开始拖动。3<Reorder.Group axis="y" values={items} onReorder={setItems}>
{items.map((item) => (
<Reorder.Item key={item.id} value={item}>
<button className="drag-handle" aria-label="拖动排序" />
<span>{item.title}</span>
</Reorder.Item>
))}
</Reorder.Group>容易做坏的地方
拖拽把手要明显。NN/g 提到,grab-handle icon 可以告诉用户对象能被拖动,也能提供一个安全的点击目标,避免误触旁边按钮。2 但把手图标没有绝对统一的行业约定,所以最好让它出现在编辑态、排序态这类明确语境里。
触屏上要留足空间。NN/g 指出,触屏没有 hover 状态,拖拽还要区分 tap、scroll 和 intentional grab;如果手指会遮住关键反馈,用户很容易不知道自己拖到了哪里。2 这也是为什么很多移动端产品只在「编辑」模式里开启拖拽排序。
无障碍不能只靠手势。NN/g 建议拖拽把手应能通过 Tab 聚焦,并向屏幕阅读器说明当前状态、可用动作和当前位置。2 如果你的列表影响任务完成顺序,比如播放队列或表单字段排序,键盘上移 / 下移按钮通常比纯拖拽更可靠。
最后是动效克制。拖动项可以有阴影和轻微缩放,但不要让整列大幅弹跳。MDN 在
transform 文档里提醒,缩放或运动动画可能触发部分人的不适,应该尊重 prefers-reduced-motion 这类减少动态的系统偏好。6Drag to Reorder 的核心不是「拖得动」,而是让用户在松手之前就看懂结果:我抓住了哪一项,它会落到哪里,松手后顺序会怎么变。做到这三点,列表才会像真的在给手指让路。
Contenido relacionado
- Inicia sesión para comentar.
