以前搞网格布局动画,那叫一个折腾。现在好了,grid-template-rows和grid-template-columns这两兄弟在所有主流浏览器里都能愉快地做过渡动画了。虽然CSS Grid布局模块 Level 1规范老早就说了支持,但三大浏览器(Chrome、Firefox、Safari)真正齐刷刷放开这个能力,也就是最近的事儿。这波操作意味着啥?意味着以前那些不敢用Grid做的交互动效,现在可以光明正大搞起来了,不用再偷偷摸摸用JS模拟。
啥是Grid行列动画
简单说,就是改变网格容器里行和列的尺寸时,能看到从A尺寸平滑变到B尺寸的过程。比如侧边栏从48px宽变成30%宽,不再“啪”一下跳过去,而是像抹了油一样顺滑。这全靠grid-template-columns和grid-template-rows这两个属性搭配transition或animation来实现。
侧边栏展开
拿最常见的两列布局举例。左边导航栏窄窄一条,鼠标一怼上去,立马撑开显示完整文字。以前得写JS监听hover然后改样式,现在纯CSS几行搞定。
HTML架子长这样:
<div class="grid">
<div class="left">菜单</div>
<div class="right">主内容区</div>
</div>第一步,把父容器变成网格:
.grid {
display: grid;
}第二步,定好两列宽度。左边窄到只有48像素,右边用auto把剩下的地儿全占了:
.grid {
display: grid;
grid-template-columns: 48px auto;
}第三步,加上过渡时间,让变化过程肉眼可见:
.grid {
display: grid;
grid-template-columns: 48px auto;
transition: 300ms;
}关键来了。hover效果不能直接写在.left上,因为要改变的是父容器.grid的列宽。这里掏出神器:has()伪类——它能根据子元素的状态来选中父元素。翻译成人话就是:“如果.grid里面有个.left正在被鼠标怼着,那就把.grid的列宽改了”。
.grid:has(.left:hover) {
grid-template-columns: 30% auto;
}就这么简单,鼠标滑过左边栏,左边宽变成30%,右边自动调整剩下70%。整个过程平滑过渡,没有半点JS影子。
有些项目喜欢用CSS变量,写法更清爽:
.grid {
display: grid;
transition: 300ms;
grid-template-columns: var(--left, 48px) auto;
}
.grid:has(.left:hover) {
--left: 30%;
}这里有个坑得提一嘴——:has()的兼容性。虽然现在主流版本都支持了,但老一点的浏览器比如Chrome 105之前、Firefox 118之前可能翻车。如果项目要照顾古董浏览器,可以考虑用JS兜底。不过话说回来,这年头谁还抱着老掉牙版本不放?
| 浏览器 | 最低版本 | 支持情况 |
|---|---|---|
| Chrome | 105 | 完全支持 |
| Firefox | 118 | 完全支持 |
| Safari | 15.4 | 完全支持 |
面板扩展效果
再上个骚操作:三块面板排排坐,鼠标悬停哪块,哪块就变宽,背景颜色也跟着渐变。这种交互适合做产品展示或者导航卡片。
HTML结构:
<div class="panels">
<div class="panel">面板A</div>
<div class="panel">面板B</div>
<div class="panel">面板C</div>
</div>CSS部分:
.panels {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
transition: 400ms;
gap: 8px;
}
.panel {
background: #ddd;
transition: background 300ms;
padding: 1rem;
}
.panels:has(.panel:hover) .panel:not(:hover) {
background: #f0f0f0;
}
.panels:has(.panel:hover) {
grid-template-columns: 1.5fr 1fr 0.8fr;
}这里有个细节:repeat()函数在某些浏览器里做过渡时会抽风,动画一顿一顿的。稳妥起见,像上面这样把三个1fr挨个写出来,别用repeat(3, 1fr)。虽然代码啰嗦了点,但动画丝滑不卡壳,值了。
鼠标放到面板B上时,整体列宽变成1.5fr 1fr 0.8fr,同时没被hover的面板背景色变淡,制造出焦点聚在中间的效果。反过来,如果只想改变当前hover的那一块,可以写成:
.panels:has(.panel:nth-child(1):hover) {
grid-template-columns: 2fr 1fr 1fr;
}
.panels:has(.panel:nth-child(2):hover) {
grid-template-columns: 1fr 2fr 1fr;
}
.panels:has(.panel:nth-child(3):hover) {
grid-template-columns: 1fr 1fr 2fr;
}分别指定每块面板悬停时的列宽分配,精准控制,绝不误伤兄弟元素。
行列动态添加
最后来一个“凭空冒出新列”的玩法。比如一个两列网格,hover时悄悄多出一列,而且不是闪现,是像从0宽度慢慢长出来。
HTML:
<div class="dynamic-grid">
<div class="col">1</div>
<div class="col">2</div>
<div class="col hidden-col">新列</div>
</div>CSS:
.dynamic-grid {
display: grid;
grid-template-columns: 1fr 1fr 0fr;
transition: 350ms;
}
.dynamic-grid:hover {
grid-template-columns: 1fr 1fr 1fr;
}
.hidden-col {
overflow: hidden;
white-space: nowrap;
}注意那个0fr——单位fr必须带上,哪怕值是0。写成0不行,浏览器会懵圈,直接当无效值处理。第三列一开始宽度为0,但DOM元素还在,Grid承认它的存在。hover时宽度变成1fr,过渡效果就出来了。
这里有个巨坑:千万别把那列设成display: none。一旦隐藏,Grid就认为这列不存在,从两列变成三列,过渡动画没法做(因为属性值个数都变了)。正确姿势是让元素存在,宽度为0,同时用overflow: hidden把内容藏起来,防止文字挤出来。
反过来,删掉一列也是同理。从三列变成两列,可以先让某一列宽度缩到0,但保留元素,视觉上“消失”,等动画结束后再移除或者隐藏。不过纯CSS做不到动画结束后再隐藏,那就让它永远占个0宽位置,也没啥影响。
Michelle Barker有个更花哨的例子——做一个响应式的动态网格,鼠标悬浮时额外弹出一列,里面塞满各种小彩蛋。原理一模一样,就是0fr到1fr的过渡,再加上一点内容动画点缀。
最后补个冷知识:grid-template-rows的动画跟列完全对称。比如做一个手风琴式的行折叠效果,把某一行高度设为0fr,hover时变成1fr,照样丝滑。唯一要注意的是行内内容溢出问题,配合overflow: hidden和min-height: 0一起食用更佳。
