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

如何为React列表项上下移动添加translateY过渡动画?

列表项上下移动动画的最佳实现方案

问题分析

你当前的实现直接通过交换数组元素触发React重渲染,但存在两个核心问题:

  • 用列表项内容作为key,交换后React会复用原有DOM节点,无法触发过渡动画;
  • 没有给元素添加动画触发的状态标记,无法精准控制动画的触发时机和方向。

实现步骤

  1. 给列表项添加唯一标识:避免用内容作为key,改用唯一ID确保React能正确识别DOM节点的变化。
  2. 跟踪移动状态:维护一个状态记录正在移动的元素ID和方向,用于给对应元素添加动画类。
  3. 控制动画生命周期:在元素交换完成后,延迟移除动画类,确保动画完整执行。
  4. 精准计算动画位移:结合列表项的高度和间距,计算正确的translateY值,避免定位偏差。

修改后的完整代码

React 代码

const { useState } = React;

// 给每个item添加唯一ID,避免用内容当key
const ITEMS = [
  { id: 1, text: "item 1" },
  { id: 2, text: "item 2" },
  { id: 3, text: "item 3" },
  { id: 4, text: "item 4" }
];

const App = () => {
  const [items, setItems] = useState(ITEMS);
  // 记录正在移动的元素:{ id: number, direction: 'up' | 'down' }
  const [movingItem, setMovingItem] = useState(null);

  const handleMoveItem = (index, direction) => {
    const updatedItems = [...items];
    const targetIndex = direction === "up" ? index - 1 : index + 1;

    if (targetIndex >= 0 && targetIndex < items.length) {
      // 标记当前移动的元素和方向
      setMovingItem({ id: updatedItems[index].id, direction });
      // 交换元素
      [updatedItems[index], updatedItems[targetIndex]] = [
        updatedItems[targetIndex],
        updatedItems[index]
      ];
      setItems(updatedItems);

      // 动画结束后清除移动状态
      setTimeout(() => setMovingItem(null), 300);
    }
  };

  return (
    <div className="wrapper">
      <ul>
        {items.map((item, index) => {
          // 判断当前元素是否正在移动,添加对应动画类
          const isMoving = movingItem?.id === item.id;
          const animationClass = isMoving ? `move-${movingItem.direction}` : "";
          
          return (
            <li key={item.id} className={`list-item ${animationClass}`}>
              {item.text}
              <div>
                <button 
                  onClick={() => handleMoveItem(index, "up")}
                  disabled={index === 0}
                >
                  up
                </button>
                <button 
                  onClick={() => handleMoveItem(index, "down")}
                  disabled={index === items.length - 1}
                >
                  down
                </button>
              </div>
            </li>
          );
        })}
      </ul>
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));

CSS 代码

:root {
  --item-gap: 1rem;
  --item-height: 60px;
}

.wrapper {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

ul {
  list-style: none;
  width: 500px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: var(--item-gap);
}

.list-item {
  text-align: center;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 1rem;
  background-color: lightblue;
  height: var(--item-height);
  transition: transform 0.3s ease;
}

/* 计算位移:元素高度 + 间距 */
.move-up {
  transform: translateY(calc(-1 * (var(--item-height) + var(--item-gap))));
}

.move-down {
  transform: translateY(calc(var(--item-height) + var(--item-gap)));
}

button {
  margin-left: 0.5rem;
  padding: 0.3rem 0.6rem;
  cursor: pointer;
}

button:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

HTML 代码

<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script>

关键说明

  • 唯一ID作为key:确保React在元素交换时能正确识别DOM节点变化,触发过渡动画;
  • 移动状态跟踪:通过movingItem状态精准控制动画类的添加和移除,确保动画只在移动时触发;
  • CSS变量计算位移:统一管理元素高度和间距,避免硬编码数值导致的定位偏差;
  • 禁用边界按钮:优化交互体验,避免无效点击。

内容的提问来源于stack exchange,提问作者Adrian

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 18:57:46