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

Firefox中CSS过渡效果间歇性失效,元素移动无平滑动画的问题求助

Firefox中CSS过渡效果间歇性失效,元素移动无平滑动画的问题求助

大家好,我最近做了一个带交互的静态页面:橙色小方块默认在金色的inner-box里,选中方块后点击「Move Out」按钮,它应该平滑过渡到紫色outer-box的右侧位置。但在Firefox里测试时遇到了头疼的问题——点击按钮后方块直接跳到目标位置,完全没有设置的4秒过渡动画;而Chrome、Edge这些Blink内核的浏览器里,动画是正常平滑的。

先把我的页面代码贴出来,方便大家排查:

HTML 结构

<div class="controls">
  <button id="moveOutBtn" disabled>Move Out</button>
  <button id="resetBtn">Reset</button>
</div>
<div class="outer-box" id="outerBox">
  <div class="inner-box" id="innerBox">
    <div class="object" style="left: 20px; top: 20px"></div>
    <div class="object" style="left: 80px; top: 80px"></div>
    <div class="object" style="left: 140px; top: 140px"></div>
  </div>
</div>

CSS 样式

body {
  background: #222;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 20px;
}

.controls {
  display: flex;
  gap: 10px;
}

button {
  padding: 10px 20px;
  font-size: 16px;
  border-radius: 8px;
  border: none;
  background: #4caf50;
  color: white;
  cursor: pointer;
  transition: background 0.3s;
}

button:hover {
  background: #45a049;
}

button:disabled {
  background: #cccccc;
  cursor: not-allowed;
}

.outer-box {
  position: relative;
  width: 500px;
  height: 400px;
  background: purple;
  border-radius: 16px;
  box-shadow: 0 0 0px 0px #fff;
  transition: box-shadow 0.2s;
  overflow: visible;
  padding: 36px;
}

.outer-box.indicate {
  box-shadow: 0 0 30px 8px #fffbe0;
}

.inner-box {
  position: absolute;
  top: 50px;
  left: 50px;
  width: 300px;
  height: 250px;
  background: gold;
  border-radius: 12px;
  z-index: 1;
  box-sizing: border-box;
  padding: 16px;
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: flex-start;
  justify-content: flex-start;
}

.object {
  width: 50px;
  height: 50px;
  background: orange;
  border-radius: 8px;
  cursor: pointer;
  position: absolute;
  user-select: none;
  box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.15);
  z-index: 2;
  border: 3px solid transparent;
  transition: left 4s ease-out, border 0.25s ease-out;
}

.object.selected {
  border: 3px solid #fff;
  box-shadow: 0 0 15px rgba(255, 255, 255, 0.5);
}

.object.outside {
  border: 3px solid #fffbe0;
  box-shadow: 0 0 18px 4px #fffbe0;
}

JavaScript 交互逻辑

const outerBox = document.getElementById("outerBox");
const innerBox = document.getElementById("innerBox");
const moveOutBtn = document.getElementById("moveOutBtn");
const resetBtn = document.getElementById("resetBtn");
const objects = document.querySelectorAll(".object");

let selectedObject = null;
let isMoving = false;

// Initialize object positions
const initialPositions = Array.from(objects).map(obj => ({
  left: obj.style.left,
  top: obj.style.top
}));

function selectObject(obj) {
  // Deselect previously selected object
  if (selectedObject) {
    selectedObject.classList.remove("selected");
  }
  // Select new object
  selectedObject = obj;
  selectedObject.classList.add("selected");
  moveOutBtn.disabled = false;
}

function resetObjects() {
  isMoving = false;
  moveOutBtn.disabled = true;
  objects.forEach((obj, index) => {
    obj.classList.remove("selected", "outside");
    obj.style.left = initialPositions[index].left;
    obj.style.top = initialPositions[index].top;
    if (obj.parentElement !== innerBox) {
      innerBox.appendChild(obj);
    }
  });
  outerBox.classList.remove("indicate");
  selectedObject = null;
}

function moveObjectOut() {
  if (!selectedObject || isMoving) {
    return;
  }
  isMoving = true;
  moveOutBtn.disabled = true;

  // Move to outer box for unrestricted movement
  const objRect = selectedObject.getBoundingClientRect();
  const outerRect = outerBox.getBoundingClientRect();
  const newLeft = objRect.left - outerRect.left;
  const newTop = objRect.top - outerRect.top;

  selectedObject.style.left = newLeft + "px";
  selectedObject.style.top = newTop + "px";
  outerBox.appendChild(selectedObject);

  // Start movement animation
  requestAnimationFrame(() => {
    selectedObject.style.left = outerRect.width - 70 + "px";
  });

  // Check for boundary crossing
  const checkPosition = setInterval(() => {
    if (!selectedObject) {
      return;
    }
    const currentRect = selectedObject.getBoundingClientRect();
    const innerRect = innerBox.getBoundingClientRect();

    /***** Alert if partially outside *****/
    const isOutside = currentRect.right > innerRect.right ||
      currentRect.left < innerRect.left ||
      currentRect.bottom > innerRect.bottom ||
      currentRect.top < innerRect.top;

    if (isOutside) {
      selectedObject.classList.add("outside");
      outerBox.classList.add("indicate");
      clearInterval(checkPosition);
      isMoving = false;
    }
  });

  globalThis.addEventListener("unload", () => {
    clearInterval(checkPosition);
  });
}

// Event Listeners
objects.forEach(obj => {
  obj.addEventListener("click", () => selectObject(obj));
});
moveOutBtn.addEventListener("click", moveObjectOut);
resetBtn.addEventListener("click", resetObjects);

问题排查与解决方案

我查了资料和做了几次测试后,发现问题根源在于Firefox对DOM节点移动后样式更新的时机处理:
当把selectedObject从innerBox append到outerBox后,Firefox会立即应用新的style.left值(在requestAnimationFrame里的那行),但此时浏览器还没“记住”元素移动后的初始位置,所以直接跳到了目标值,没有触发过渡。而Blink内核浏览器会自动在DOM修改和样式更新之间插入一个微任务延迟,让过渡能捕捉到初始状态。

解决方法很简单,在把元素添加到outerBox后,强制触发一次重排,让浏览器先计算并记录元素的初始布局,再应用目标位置样式:

修改moveObjectOut函数里的这段代码:

outerBox.appendChild(selectedObject);
// 强制触发重排,让Firefox捕捉初始位置
void selectedObject.offsetWidth;
// Start movement animation
requestAnimationFrame(() => {
  selectedObject.style.left = outerRect.width - 70 + "px";
});

这里的void selectedObject.offsetWidth是关键:读取元素的offsetWidth会迫使浏览器立即执行当前所有的样式更新和重排操作,这样后续修改style.left时,浏览器就能识别出“初始位置→目标位置”的变化,从而触发我们设置的left 4s ease-out过渡动画。

修改后再在Firefox里测试,橙色方块就能像在Chrome里一样平滑移动到右侧了,问题解决!

如果大家还有其他思路或者更优雅的解法,欢迎一起讨论~

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 14:13:06