You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用JavaScript实现列表项的上下移动动画效果?

实现列表项上下移动的平滑动画

核心思路

实现平滑动画的关键是先让元素视觉上交换位置,再更新DOM结构——直接操作DOM会导致布局瞬间跳动,而通过CSS过渡+临时定位,可以让移动过程更自然。下面是完整的实现方案,适合新手理解和复用:


1. HTML结构

先搭建基础的列表结构,每个列表项包含内容和上下移动按钮:

<ul class="sortable-list">
  <li class="list-item">
    <span>列表项 1</span>
    <div class="controls">
      <button class="move-btn move-up">↑</button>
      <button class="move-btn move-down">↓</button>
    </div>
  </li>
  <li class="list-item">
    <span>列表项 2</span>
    <div class="controls">
      <button class="move-btn move-up">↑</button>
      <button class="move-btn move-down">↓</button>
    </div>
  </li>
  <li class="list-item">
    <span>列表项 3</span>
    <div class="controls">
      <button class="move-btn move-up">↑</button>
      <button class="move-btn move-down">↓</button>
    </div>
  </li>
</ul>

2. CSS样式

重点设置过渡属性和动画时的临时定位,避免布局跳动:

.sortable-list {
  list-style: none;
  padding: 0;
  width: 320px;
  margin: 20px auto;
  /* 给列表设置相对定位,让动画项的absolute基于列表定位 */
  position: relative;
}

.list-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  margin: 8px 0;
  background: #f5f5f5;
  border-radius: 6px;
  /* 核心:设置过渡动画的属性和时长 */
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease;
  /* 默认层级,动画时提升层级避免遮挡 */
  z-index: 1;
}

/* 动画过程中临时添加的类:固定元素位置,防止布局偏移 */
.list-item.animating {
  position: absolute;
  width: calc(100% - 32px); /* 对应左右padding的16px*2 */
  opacity: 0.9;
  z-index: 10;
}

.move-btn {
  border: none;
  background: #e0e0e0;
  width: 28px;
  height: 28px;
  border-radius: 4px;
  cursor: pointer;
  margin-left: 4px;
}

.move-btn:hover {
  background: #bdbdbd;
}

3. JavaScript逻辑

实现按钮点击事件、动画触发和DOM交换的完整流程:

// 批量绑定按钮事件
document.querySelectorAll('.move-up').forEach(btn => {
  btn.addEventListener('click', () => handleMove(btn, 'up'));
});

document.querySelectorAll('.move-down').forEach(btn => {
  btn.addEventListener('click', () => handleMove(btn, 'down'));
});

/**
 * 处理移动逻辑
 * @param {HTMLElement} btn 点击的按钮
 * @param {string} direction 移动方向:up/down
 */
function handleMove(btn, direction) {
  const currentItem = btn.closest('.list-item');
  let targetItem;

  // 判断边界情况,避免无效操作
  if (direction === 'up') {
    targetItem = currentItem.previousElementSibling;
    if (!targetItem || !targetItem.classList.contains('list-item')) return;
  } else {
    targetItem = currentItem.nextElementSibling;
    if (!targetItem || !targetItem.classList.contains('list-item')) return;
  }

  // 启动交换动画
  animateSwap(currentItem, targetItem);
}

/**
 * 执行元素交换动画
 * @param {HTMLElement} itemA 当前元素
 * @param {HTMLElement} itemB 目标元素
 */
function animateSwap(itemA, itemB) {
  // 获取两个元素的位置信息(相对于视口)
  const rectA = itemA.getBoundingClientRect();
  const rectB = itemB.getBoundingClientRect();
  // 计算Y轴偏移量:A需要移动到B的位置,B需要移动到A的位置
  const deltaY = rectB.top - rectA.top;

  // 添加动画类,固定位置
  itemA.classList.add('animating');
  itemB.classList.add('animating');

  // 设置transform,触发过渡动画
  itemA.style.transform = `translateY(${deltaY}px)`;
  itemB.style.transform = `translateY(${-deltaY}px)`;

  // 监听过渡结束,完成DOM交换
  const onTransitionEnd = () => {
    // 交换DOM节点位置
    itemA.parentNode.insertBefore(itemA, itemB);
    // 修正:如果是向下移动,需要插入到目标元素的下一个位置
    if (itemA.nextElementSibling === itemB) {
      itemA.parentNode.insertBefore(itemB, itemA);
    } else {
      itemA.parentNode.insertBefore(itemB, itemA.nextElementSibling);
    }

    // 重置样式和类,恢复正常状态
    itemA.classList.remove('animating');
    itemB.classList.remove('animating');
    itemA.style.transform = '';
    itemB.style.transform = '';

    // 移除事件监听,避免重复触发
    itemA.removeEventListener('transitionend', onTransitionEnd);
    itemB.removeEventListener('transitionend', onTransitionEnd);
  };

  // 绑定过渡结束事件
  itemA.addEventListener('transitionend', onTransitionEnd);
  itemB.addEventListener('transitionend', onTransitionEnd);
}

关键细节说明

  • 临时定位(absolute):动画过程中给元素添加animating类,固定其位置,避免DOM操作导致的布局跳动,动画结束后再实际交换节点。
  • 过渡曲线:用cubic-bezier(0.4, 0, 0.2, 1)替代默认的ease,让动画更符合自然运动规律。
  • 边界判断:检查当前元素是否为第一个/最后一个,防止点击无效按钮时报错。
  • 事件清理:动画结束后必须移除transitionend监听,避免内存泄漏和后续操作异常。

内容的提问来源于stack exchange,提问作者Tobias von Falkenhayn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 22:35:01