拖动窗口时它一点都不肯动:断点、过渡与重排之间怎么选
一、断点跨越只有一瞬间,中间根本没有值
先说一个反直觉的事实:不是 CSS 过渡不肯跟窗口走,而是窗口拖动的大部分过程里,压根没有任何属性的目标值发生变化。
媒体查询是一组开关。视口宽度 900px 时生效的是 A 套规则,901px 时生效的仍然是 A 套,一路拖到 1024px 才”啪”地换成 B 套。换的那一瞬间,width 这类属性的计算值从 A 的终值跳到 B 的终值;如果你没给它声明过渡,浏览器就直接按新值重排,这就是那个跳变。php.cn 上有条讲视口尺寸变化平滑过渡的问答说得很直接:媒体查询切换之所以是瞬时的,因为没有过渡声明。
那加上 transition 呢?跨断点的那一刻确实会补间一次。但你想要的”跟着鼠标连续动”依然不会出现——因为两个断点之间的 100 多像素里,目标值始终是同一个,没有变化就没有动画。再叠一个副作用:拖得快的话,短时间内连续跨越多个断点,上一次的补间还没跑完就被新目标打断重启,视觉上变成抖,而不是平滑。
所以要区分两个不同的需求,它们的答案完全不同:
- “形态切换不要太生硬”——侧栏收起、卡片从双列变单列,这是两个离散状态之间的问题,用过渡就够。
- “尺寸跟着窗口连续变化”——这要求元素的值本身就是视口的函数,属于连续量的事,
transition不是这件事的工具。

二、让尺寸成为连续量:clamp() 和 vw 是第一步
看板类页面的主体区域,我们尽量不写成”某段宽度内固定多少像素”,而是让它直接由视口驱动:
.panel-side { |
vw 负责连续,clamp() 负责上下界。这样拖动窗口时元素是真正一帧一帧跟着走的,不需要任何过渡声明,也不会有跳变。混合单位(0.55vw + 0.5rem)用来把斜率调小一点,避免大屏上字号增长过快。
代价也要认清:纯连续方案没有”形态”的概念。窗口压到很窄时,一个为 3 列设计的卡片网格会挤成不能看的宽度。所以 grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)) 这类自带换行能力的写法是我们的默认搭配——它把”什么时候减列”这件事交给了可用宽度本身,而不是交给某个断点数字。
三、自定义属性 + transition:真正在动的仍是解析后的值
问答里另一条常见思路是把断点值收进 CSS 自定义属性,切换时让实际属性去过渡:
:root { --side-w: 240px; } |
要说明清楚动的是什么:自定义属性本身不参与补间。未注册的自定义属性是”非可视属性”,值变化按离散处理;能被过渡的是 width 这个真实存在的可动画属性,它在 var() 解析后的计算值发生变化时触发过渡。换句话说,这个写法并没有让过渡跟窗口连续走,它只是把断点集中管理、避免在多处重复写同一组数字——工程上很有价值,但不是”平滑跟随”的解法。
如果你确实需要过渡自定义属性本身(比如一个用作间距或角度的倍数),要用 @property 显式注册并给出 syntax 和 initial-value,注册后它才具备可插值的能力。这条路在工业现场的一体机浏览器、老版本内核上支持情况不一,我们的规矩是先实测再依赖,不作为主干方案。
还有一个组合用法值得知道:如果用 requestAnimationFrame 每帧把自定义属性写成”当前视口算出来的目标值”,同时给真实属性留一段 transition,实际渲染值会持续追赶目标值,形成一种阻尼式平滑。效果确实顺,但代价是只要还在拖动,动画就一直处于未完成态,收尾时你得保证停止改值——否则元素会一直在微动。看板页我们慎用这招,因为持续重排会干扰读数。
四、需要离散形态时:换 class 的时机交给观察器
形态切换(展开/收起、紧凑/宽松、图表换布局)不要靠一堆媒体查询硬写,也不要放在 resize 事件里算样式。三种触发方式按精度选:
matchMedia:断点判断和 CSS 保持一致的唯一办法。用它监听同一个查询串,命中再切 class,避免”CSS 已经换了但 JS 的状态没换”。ResizeObserver:看的是元素自己的盒子尺寸,不是视口。侧栏宽度变化往往由父容器决定,跟窗口宽度并不等价;观察器在布局之后、绘制之前交付回调,一帧内合并通知,比resize事件那种在拖拽期间高频连发的路子稳定。注意它有循环触发风险——回调里改了被观察元素的尺寸就会再触发自己,我们统一用一个布尔闸门避免。IntersectionObserver:更适合”元素滚动进某个区域才换形态”,比如长报表里表格在窄屏下从多列改为堆叠展示,进入视口才展开,避免一次性给全部行做布局。
组件级的形态,能用容器查询就用容器查询(@container),它天生按容器宽度而不是视口宽度决策,看板里”同一块卡片放进宽侧栏和窄主区要长得不一样”就是这个场景。同样是那句:老内核支持不齐,交付前在目标设备上验一遍。
五、别给几何属性做大范围过渡
来源给的示例是这一行:transition: top 500ms ease, transform 500ms ease;,同时提醒不要用 all,并推荐走硬件加速的 transform。这个方向我们完全同意,再补一层机制上的解释,方便判断边界。
| 属性 | 过渡时的主要代价 | 适合范围 |
|---|---|---|
width / height |
每帧触发布局,且会连带子树重排 | 小范围、单元素、明确终值 |
top / left / margin |
同上,布局线程 | 短距位移 |
padding / gap |
布局 | 数值差很小时可接受 |
transform |
合成层处理,不进布局 | 位移与缩放的首选 |
opacity |
合成层 | 淡入淡出 |
filter / box-shadow |
每帧重绘,成本随面积涨 | 别用于持续动画 |
all |
不知道会带上什么 | 不要用 |
结论就是:范围大、距离远、数量多的动画一律换成 transform。但看板有个特殊之处——用 scale() 把整块面板”缩小适配”,文字和数字会被当作位图插值,读数发虚,工业场景里这是不可接受的。所以缩放只用于图形装饰,涉及数据的区域我们坚持用 clamp() 改实际字号和宽度,宁可付出布局成本也不牺牲可读性。
六、投屏与笔记本之间,我们定的三条规矩
现场交付时,同一块看板会在三种载体上打开:会议室的固定分辨率投屏、笔记本上边拖边看、以及车间一体机上锁定的窗口。三种载体的诉求互相冲突,于是我们把它写成能评审的规矩。
- 主干尺寸走连续量,断点只做形态切换。所有跟面积相关的宽高优先
clamp()配vw,媒体查询只用来决定”几列、侧栏在不在、图例位置”。这样投屏和笔记本之间拖窗口,画面是连续适配的,不依赖动画。 - 投屏分辨率当作固定终值处理,而不是当作动画起点。会议室设备实际就是几个固定档位(常见的 1280、1366、1920 宽),断点必须跨过这些值上下留余量。给这中间做补间没有意义,因为不存在拖动过程;真正需要做的是”接上投影那一帧就落在正确形态”。
- 动画预算只花在用户主动触发的切换上。侧栏展开、卡片详情弹出,这些有明确操作意图的动作给 300 到 500 毫秒的过渡,属性写死具体名字;
resize期间不加过渡,直接按终值重排。理由很实际——持续补间会让图表重绘与布局互相排队,用户看到的是数字在跳,读数不可信,比生硬的切换更糟。
顺带一条经常被忘的收尾:给过渡设终值时不要用百分比高度去”跟着视口”,视口相关的量优先 dvh/svh 这类随浏览器 UI 变化的单位,或者干脆交给观察器写自定义属性。移动端浏览器地址栏收起时,vh 是不跟着变的,那个高度差足以让一屏表格被截掉一行。
回到最初那句”为什么它不跟着窗口动”:因为断点之间没有新的目标值,动画就没有可插值的东西。要跟随,就让尺寸成为视口的函数;要不生硬,就让离散切换发生在用户主动的那一次。把这两件事分开,视口适配的动效问题基本就不再是问题了。