如何实现两个div盒子沿半圆弧路径交换位置的CSS动画
实现方案
核心思路
- 基础布局用 Flex 实现多个 div 水平等距排列
- 交换动画核心是让两个元素分别沿上下两个半圆路径反向移动,避免碰撞
- 动画结束后真实交换两个 DOM 元素的位置,复位动画状态方便下次触发
完整可运行代码
HTML 结构
<div class="container"> <div class="box">1</div> <div class="box">2</div> <div class="box">3</div> <div class="box">4</div> <div class="box">5</div> </div> <button id="swapBtn">随机交换两个盒子</button>
CSS 样式
.container { display: flex; gap: 20px; margin: 100px auto; padding: 20px; width: fit-content; position: relative; } .box { width: 80px; height: 80px; background: #4e8cff; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 24px; border-radius: 8px; position: relative; offset-rotate: 0deg; transition: none; } /* 沿上半圆移动动画 */ .animate-top { animation: moveTop 1s ease-in-out forwards; } /* 沿下半圆移动动画 */ .animate-bottom { animation: moveBottom 1s ease-in-out forwards; } @keyframes moveTop { 0% { transform: translate(0, 0); } 50% { transform: translate(calc(var(--distance) / 2), calc(-1 * var(--radius))); } 100% { transform: translate(var(--distance), 0); } } @keyframes moveBottom { 0% { transform: translate(0, 0); } 50% { transform: translate(calc(var(--distance) / -2), var(--radius)); } 100% { transform: translate(calc(var(--distance) * -1), 0); } } #swapBtn { display: block; margin: 0 auto; padding: 10px 20px; font-size: 16px; cursor: pointer; }
JS 逻辑
const swapBtn = document.getElementById('swapBtn'); const container = document.querySelector('.container'); let boxes = Array.from(document.querySelectorAll('.box')); swapBtn.addEventListener('click', () => { // 随机选取两个不重复的盒子 const [idx1, idx2] = getTwoRandomIndex(boxes.length); let boxA = boxes[idx1]; let boxB = boxes[idx2]; // 保证boxA是位置更靠左的盒子 if (boxA.offsetLeft > boxB.offsetLeft) { [boxA, boxB] = [boxB, boxA]; } // 计算两个盒子的水平间距、半圆半径 const distance = boxB.offsetLeft - boxA.offsetLeft; const radius = distance / 2; // 注入动画需要的CSS变量 boxA.style.setProperty('--distance', `${distance}px`); boxA.style.setProperty('--radius', `${radius}px`); boxB.style.setProperty('--distance', `${distance}px`); boxB.style.setProperty('--radius', `${radius}px`); // 触发动画 boxA.classList.add('animate-top'); boxB.classList.add('animate-bottom'); // 动画结束后交换DOM位置,复位样式 setTimeout(() => { if (idx1 < idx2) { container.insertBefore(boxB, boxA); } else { container.insertBefore(boxA, boxB); } // 清除动画状态 boxA.classList.remove('animate-top'); boxB.classList.remove('animate-bottom'); boxA.style.removeProperty('--distance'); boxA.style.removeProperty('--radius'); boxB.style.removeProperty('--distance'); boxB.style.removeProperty('--radius'); // 更新盒子数组顺序 [boxes[idx1], boxes[idx2]] = [boxes[idx2], boxes[idx1]]; }, 1000); // 和CSS动画时长保持一致 }); // 生成两个不重复的随机索引 function getTwoRandomIndex(max) { let idx1 = Math.floor(Math.random() * max); let idx2 = Math.floor(Math.random() * max); while (idx1 === idx2) { idx2 = Math.floor(Math.random() * max); } return [idx1, idx2]; }
关键说明
- 采用
transform组合实现半圆路径,兼容性比offset-path方案更好,无需适配低版本浏览器 - 动画时长、圆弧弧度、盒子样式都可以通过调整对应CSS参数自定义
- 动画过程中所有偏移都是元素自身的相对位移,不会影响其他盒子的布局
内容的提问来源于stack exchange,提问作者Harsh Vardhan Singh
相关产品推荐
相关产品推荐

