CSS transition 不生效,90% 的情况属于三类原因:属性本身不可动画、初始值与目标值在同一次渲染帧内被浏览器合并、transition 声明写错位置或被更高优先级覆盖。按“属性是否可过渡、是否被强制重排、声明写在哪”这三步顺序排查,基本都能定位。
transition 不生效先查哪三件事?
结论:先确认这三件事,再谈调参——属性名拼写与可动画性、是否同一帧内修改样式、transition 是否写在基础状态选择器上。
第一,transition-property 里的属性必须可插值。color、opacity、transform、width(数值到数值)可以;display、background-image 渐变、height: auto 之间的切换在旧语法下不行。
第二,浏览器在每帧只做一次样式计算,如果你用 JS 在同一个任务里先赋值 opacity: 0 再赋值 opacity: 1,浏览器合并成一次,看起来“值没变”,自然不会过渡。
第三,transition 要写在元素的基础状态上,不是 :hover 里。写成 .btn:hover { transition: 0.3s; } 的后果是:移入有过渡、移出瞬间跳回,很多人误判成“完全没生效”。
为什么改完样式立刻过渡不生效?怎么强制重排?
结论:需要让浏览器先把初始样式落到渲染树,再改目标值;最稳的做法是 requestAnimationFrame 嵌套两层,其次读一次 offsetHeight 强制回流。
最小可用写法:
el.style.opacity = '0';
requestAnimationFrame(() => {
requestAnimationFrame(() => { el.style.opacity = '1'; });
});
单层 requestAnimationFrame 在部分浏览器与 React 的批处理场景下仍会被合并,双层更保险。经典写法 el.style.opacity = '0'; void el.offsetHeight; el.style.opacity = '1'; 同样有效,因为读取 offsetHeight 会强制同步回流,把第一帧样式落实。缺点是触发一次布局,在长列表里连续调用会造成掉帧,优先用双 rAF。
React、Vue 里同一轮 setState 修改两个 class 也属于这种情况,把第二处改动放进 useEffect 或 nextTick 后的下一帧再执行。
哪些 CSS 属性不能过渡?display 和 height:auto 怎么办?
结论:不可插值的属性无法用传统 transition 过渡,display 与 height: auto 是最高频的两个坑。
display 从 none 到 block:Chrome 117 起支持 transition-behavior: allow-discrete 配合 @starting-style,可以让 display 参与过渡:
.panel {
display: none;
opacity: 0;
transition: opacity .3s, display .3s allow-discrete;
}
.panel.open { display: block; opacity: 1; }
@starting-style { .panel.open { opacity: 0; } }
height: auto 的展开动画:Chromium 129 起支持 interpolate-size: allow-keywords,直接对 height: auto 做过渡;在此之前只能用 max-height(给一个足够大的固定值,代价是动画节奏不均匀)或 JS 读取 scrollHeight 后写死像素值再过渡到 0。
background-image 的 linear-gradient 之间不能过渡,替代方案是叠一层伪元素改 opacity。
transition 写在 hover 里为什么错?选择器和优先级怎么排查?
结论:transition 属于“状态无关”的声明,必须放在基础选择器里;写在 :hover、.active 里等于只给单向过渡。
正确写法:
.btn { transition: background-color .2s ease, transform .2s ease; }
.btn:hover { background-color: #0a7; transform: translateY(-2px); }
浏览器对该元素的过渡属性使用改动前的 style来读取 transition 值,所以基础状态必须有声明。另外三个高频细节:
transition: 0.3s; 单独写时第一个时间值是 duration 没错,但如果写成 transition: ease 0.3s 这种顺序错乱的简写,声明会被整条丢弃,transition-duration 回落到 0s。
- 内联样式里的
!important 或 CSS-in-JS 生成的重复声明会覆盖过渡属性,打开 DevTools 的 Computed 面板搜索 transition,看最终值是不是 all 0s ease 0s。
- 动画属性本身如果被
animation 或 will-change: transform 创建的合成层接管,行为会偏移,排查时先注释掉 animation。
DevTools 里怎么快速验证过渡有没有触发?
结论:用 Animations 面板录制,能看到过渡条就说明声明生效、问题在时序或属性;完全没条就是声明或可动画性问题。
操作路径:打开 DevTools → 右上角 More tools → Animations → 点击录制 → 触发交互。同时勾选 Rendering 面板里的 Paint flashing,可以确认过渡期间是否真的在重绘。如果 Animations 面板里出现 0.3s 的过渡条但视觉上没变化,通常是元素被父级 overflow: hidden 裁掉、z-index 被遮挡或 opacity 已经接近 1。
总结一句:transition 不生效先判断属性能不能插值,再确认两次样式改动是否被合并到同一帧(用双 rAF 或 offsetHeight 拆开),最后检查 transition 是否写在基础选择器且没被覆盖;display 和 height: auto 这两个历史遗留问题,现在分别有 transition-behavior: allow-discrete 与 interpolate-size: allow-keywords 可以用。