跳到主要内容

Framer Motion 布局动画隔离

产生这个现象的原因是:framer-motionlayout 动画默认是基于全局视口(Viewport)来计算坐标的。当 Tabs 上方的元素高度发生变化,把 Tabs 整个“顶下来”时,Tabs 内部的滑块在全局视口中的 YY 轴坐标客观上发生了改变(比如从 Y=100 变成了 Y=200)。framer-motion 捕捉到了这个坐标变化,就会自作多情地为你触发一个下落位移的过渡动画。

要让滑块完全无视整个容器被推下的位移,只在 Tabs 内部的局部空间中发生动画,最标准、最彻底的解决方案是使用 layoutRoot

终极解决方案:使用 <motion.div layoutRoot> 隔离坐标系

Framer Motion 提供了一个专门解决此类“坐标系污染”问题的属性:layoutRoot

只要用 <motion.div layoutRoot> 将你的 Tabs 容器包裹起来,它就会在内部形成一个独立的布局测量参照系。此时,滑块的坐标计算将相对于这个 Root,而不是整个浏览器窗口。这样一来,无论外部怎么把容器“顶下来”,滑块相对 Root 的 YY 轴坐标都没有变,自然就不会触发意外的下落动画了。

代码示例

import { useState } from "react";
import { motion, LayoutGroup } from "framer-motion";

function MyIsolatedTabs(){
const [activeTab, setActiveTab] = useState(0);

return (
// 🔥 关键点:将外层容器替换为 motion.div 并加上 layoutRoot 和 layout 属性
<motion.div layoutRoot layout className="tabs-container">
<LayoutGroup id="my-tabs">
<div className="flex relative">
{[0, 1, 2].map((index) => (
<button
key={index}
onClick={() => setActiveTab(index)}
className="relative p-4 z-10"
>
Tab {index}
{activeTab === index && (
<motion.div
layoutId="active-indicator"
// 依然保留 position 防止跨层级时的宽高拉伸干扰
layout="position"
className="absolute bottom-0 left-0 right-0 h-1 bg-blue-500"
/>
)}
</button>
))}
</div>
</LayoutGroup>
</motion.div>
);
}

需要在 root 容器加 layoutRootlayout (两者都需要),你的滑块就会变得异常稳定,只在你点击切换 Tab 时执行水平滑动。