列表项增删无过渡动画的根本原因是DOM突变触发强制重排,CSS无法捕获中间状态;需用FLIP技术将布局变化转为transform/opacity等可动画属性,并严格把控读取、回滚、切换、播放四步时机。

列表项增删时元素直接消失,没有过渡动画
HTML 原生的 <ul> / <ol> 本身不支持对子元素增删做 CSS 过渡,因为 display: none、DOM 移除或插入瞬间触发重排,CSS 动画无法捕获中间状态。想让列表“滑入滑出”或“高度渐变”,必须绕过 DOM 突变,用 FLIP 技术把布局变化“拍平”成可动画的属性变化。
核心思路是:先读(First)当前位置/尺寸 → 应用新状态(Last),但立即强制回滚到旧视觉状态(比如设 transform: scaleY(1))→ 再用 requestAnimationFrame 切换到目标状态(Invert)→ 最后播放动画(Play)。
实操建议:
- 给每个
<li>加唯一key(如data-key="item-123"),便于定位元素 - 增删前,用
getBoundingClientRect()记录所有相关项的位置和高度 - 插入新
<li>后,先设style.transform = "scaleY(0)"和style.opacity = "0",再offsetHeight强制重排 - 立刻改回
transform: scaleY(1)+opacity: 1,并加transition: transform 0.25s ease, opacity 0.25s ease
用 height 过渡实现列表项收起,但高度算不准
height: auto 不能被 CSS 过渡,直接写 height: 0 → height: auto 会跳变。常见错误是硬编码高度值,或依赖 scrollHeight 却忽略 padding/margin/border。
立即学习“前端免费学习笔记(深入)”;
更稳的做法是用 max-height 代替 height,并确保初始值足够覆盖最大可能内容:
- 设
max-height: 0+overflow: hidden实现收起 - 展开时设
max-height: 500px(选一个安全上限,比如预估最多 10 行文本) - 避免用
em或rem单位——字体缩放时max-height不同步变化,容易截断 - 若内容动态加载,需在数据就绪后重新计算并更新
max-height,否则动画卡在半途
React/Vue 中用 transition-group 但列表错位或闪动
框架封装的 <TransitionGroup>(React)或 <transition-group>(Vue)本质也是 FLIP,但默认只处理 enter/leave,不处理 list reordering(比如拖拽排序、filter 过滤后顺序变更)。错位常因 key 重复或未正确绑定,闪动则多因父容器未设 position: relative 导致绝对定位动画元素脱离文档流。
关键检查点:
- 每个
<li>的key必须稳定且唯一——禁止用数组 index,尤其涉及 filter/sort - 为
<transition-group>容器加style="position: relative",防止 leave 阶段元素 absolute 定位后影响其他项布局 - Vue 中若用
v-for+key,确保 key 来自数据字段(如:key="item.id"),不是计算属性或临时生成 - React 中若配合
useTransition,注意isPending状态不要误触 layout shift
移动端 Safari 上 FLIP 动画卡顿或不触发
iOS Safari 对 transform + opacity 的硬件加速支持较好,但对 max-height、margin、padding 过渡非常不友好,且 requestAnimationFrame 在后台标签页会被节流。
保底方案:
- 只对
transform和opacity做动画,禁用所有基于盒模型尺寸的过渡(height、margin、padding) - 加
will-change: transform, opacity提前提示渲染引擎 - 动画时长控制在 200–300ms,过长易被系统判定为“非响应”而降帧
- 测试时务必在真机 Safari 打开,模拟器无法复现部分渲染 bug
getBoundingClientRect 要在 layout 之前读,offsetHeight 要在样式变更后立刻调用,差一帧就失效。多数失败案例,都是因为把“读位置”和“改样式”放在了同一个事件循环里,没留出重排机会。



















