CSS Grid 与动画的深度结合:网格轨道的动态过渡与交互效果
CSS Grid 与动画的深度结合网格轨道的动态过渡与交互效果一、Grid 布局是静态的一位设计师的挑战书设计师指着 Figma 中的一个交互原型问我侧边栏展开时内容区的网格从 3 列变成 2 列能像 Apple Music 那样平滑过渡吗我下意识地回答Grid 列数变了就是 layout 重建没法做过渡动画。回去查了 spec 之后发现我错了。CSS Grid Layout Module Level 1 规范中grid-template-columns和grid-template-rows是明确支持动画的作为 track list 的 interpolation。只是浏览器实现滞后Chrome 107、Firefox 112、Safari 16.2 才开始支持。这意味着Grid 的轨道可以像width一样平滑过渡。三列变两列不再是一次生硬的布局跳变而是一列优雅地收缩消失。二、Grid 轨道动画的底层机制核心约束只有两个都是可计算的数值时Grid 轨道才能做插值动画。1fr → 0fr或200px → 0px— 可以200px → auto或auto → 1fr— 不行auto 没有数值表示minmax(100px, 1fr) → 0px— 不行三、生产级实现/* * Grid 轨道动画的前提条件 * 1. 起始值和结束值都必须是固定数值px, fr, % 等 * 2. 必须显式声明 transition 属性 * 3. 容器必须保持为 grid 或 inline-grid display */ .grid-with-animation { display: grid; /* 初始状态3 列均分 */ grid-template-columns: 1fr 1fr 1fr; /* * 注册 grid-template-columns 的过渡动画 * 时长 400ms缓动函数 ease-in-out * — 这是让轨道动起来的关键 */ transition: grid-template-columns 400ms cubic-bezier(0.4, 0, 0.2, 1); gap: 16px; padding: 16px; } /* 侧边栏展开时压缩为 2 列 */ .grid-with-animation.sidebar-open { grid-template-columns: 1fr 1fr; } /* * 让单个轨道收缩为 0 * 必须用 0fr 而非 0px——因为 gap 在 0px 轨道旁仍然生效 * 而 0fr 的轨道真正消失gap 也会被压缩到 0 */ .grid-with-animation.collapse-last { grid-template-columns: 1fr 1fr 0fr; }// Grid 轨道过渡动画 React Hook // 解决 Grid 轨道数值不可动画场景auto/minmax的降级方案 import { useState, useEffect, useRef, CSSProperties } from react; interface GridTransitionConfig { /** 列数 */ columns: number; /** 动画时长 ms */ duration?: number; /** 是否启用浏览器不支持时降级 */ enabled?: boolean; } /** * useGridTransition Hook * * 设计意图 * 当浏览器支持 grid-template-columns 动画时直接使用原生 transition * 不支持时使用 requestAnimationFrame 逐帧计算宽度做降级 * * 检测方式CSS.supports 不能直接检测轨道动画支持 * 通过创建一个测试元素并尝试获取 computed style 来判断 */ function useGridTransition({ columns, duration 300, enabled true }: GridTransitionConfig): CSSProperties { const [supportsGridAnimation, setSupportsGridAnimation] useState(false); const [currentColumns, setCurrentColumns] useState(columns); // 检测浏览器是否支持 grid-template-columns transition useEffect(() { async function detectSupport() { // 创建测试容器 const testEl document.createElement(div); testEl.style.cssText display: grid; grid-template-columns: 1fr 1fr; transition: grid-template-columns ${duration}ms; position: absolute; visibility: hidden; ; document.body.appendChild(testEl); // 触发动画3列 → 2列 requestAnimationFrame(() { testEl.style.gridTemplateColumns 1fr 1fr 1fr; requestAnimationFrame(() { testEl.style.gridTemplateColumns 1fr; // 检查 computed style 是否反映插值 const computed getComputedStyle(testEl); // 如果浏览器在做插值computed value 会是一个中间值既不是 3fr 也不是 1fr const gridCols computed.gridTemplateColumns; const parts gridCols.split( ).filter(Boolean); // 3 列 → 1 列的过渡中如果浏览器支持 // 中间态可能会显示为多列浏览器内部插值 // 简化判断grid-template-columns 的值不等于初始值也不等于目标值 setSupportsGridAnimation( parts.length 1 parts.length 3 ); document.body.removeChild(testEl); }); }); } if (enabled) { detectSupport(); } }, [enabled]); // 不支持 Grid 动画 → 降级为 JS 动画 useEffect(() { if (supportsGridAnimation) { setCurrentColumns(columns); return; } // rAF 逐帧过渡列数非像素级动画而是渐变 track 数量——这其实做不到 // 降级策略直接切换 opacity 淡入淡出掩饰 setCurrentColumns(columns); }, [columns, supportsGridAnimation]); return { display: grid, gridTemplateColumns: repeat(${currentColumns}, 1fr), // 只在浏览器支持时才加 transition ...(supportsGridAnimation { transition: grid-template-columns ${duration}ms cubic-bezier(0.4, 0, 0.2, 1) }) }; }/* * * 不支持 Grid 动画的降级方案 * * * 当 Grid 需要做 smooth transition 但浏览器不支持时 * 用绝对定位层 opacity 淡入淡出做视觉欺骗 * * 1. 保留原布局3 列 Grid计算子项的目标位置 * 2. 创建绝对定位层把子项从旧位置animate 到新位置 * 3. 动画结束后切换 Grid 列数移除定位层 */ /* 浮动过渡层脱离 Grid 流纯视觉过渡 */ .floating-transition-layer { position: fixed; /* * 从 Grid 的绝对位置通过 getBoundingClientRect 获取 * 到目标位置的 transform 动画 */ z-index: 10; pointer-events: none; /* 不拦截鼠标事件 */ /* * transition 属性只做 transform 和 opacity不触发 Layout * 这是高性能降级方案的关键——一切都在合成器线程完成 */ transition: transform 400ms cubic-bezier(0.4, 0, 0.2, 1), opacity 300ms ease-in-out; } /* 降级 JS 核心逻辑 */ /* * function gridTransitionFallback(container, fromCols, toCols) { * // 1. 记录所有子项在 fromCols 下的位置 (getBoundingClientRect) * // 2. 切换到 toCols 的 grid-template-columns * // 3. 记录切换后的位置 * // 4. 对每个子项创建 floating 副本从旧位置动画到新位置 * // 5. 动画结束后移除 floating 副本 * } */四、现实中的约束清单auto值永远不可动画。如果你的 Grid 中有一个auto列grid-template-columns: 1fr auto 1fr中间列的收缩无法做过渡。必须把auto改成具体的min-content或固定值。gap属性不支持直接过渡到 0。当轨道收缩为0fr时gap 不会随之消失——Chrome 中 gap 独立于轨道渲染。解决办法是用column-gap: 0的 transition 配合轨道动画。子项的内容回流。轨道收窄时子项内的文字可能换行、图片可能缩放。这些内容级别的 Layout 变化参与不了 Grid 轨道动画会表现为文字突然跳到下一行。解决方案对子项设置overflow: hiddenwhite-space: nowrap或者接受这个视觉不一致。性能成本。Grid 轨道过渡触发的是 Layout 级动画不是 Composite-only在低端设备上可能掉帧。对于高频交互如拖拽侧边栏建议用transform替代 Grid 动画。五、总结CSS Grid 的轨道动画是浏览器近两年才认真实现的能力支持不完整但已经可用所有轨道值都必须是可计算数值——1fr → 0fr可以auto → 1fr不行必须显式添加transition声明——不和 width/height 一样有默认过渡0fr0px——0fr让轨道真正消失包括 gap0px会留 gap 残影不支持时用 FLIP 模式降级——记录旧位置 → 切换布局 → 记录新位置 → transform 动画Grid Animation 的结合让响应式布局不再是跳变式布局。但注意它的位置在 Layout 阶段性能开销大只适合低频触发如侧边栏展开不适合高频交互。