CSS Grid动画终于来了,怎么让侧边栏丝滑展开又不用JS?

3,088字
13–20 分钟
in

以前搞网格布局动画,那叫一个折腾。现在好了,grid-template-rowsgrid-template-columns这两兄弟在所有主流浏览器里都能愉快地做过渡动画了。虽然CSS Grid布局模块 Level 1规范老早就说了支持,但三大浏览器(Chrome、Firefox、Safari)真正齐刷刷放开这个能力,也就是最近的事儿。这波操作意味着啥?意味着以前那些不敢用Grid做的交互动效,现在可以光明正大搞起来了,不用再偷偷摸摸用JS模拟。

目录

啥是Grid行列动画

简单说,就是改变网格容器里行和列的尺寸时,能看到从A尺寸平滑变到B尺寸的过程。比如侧边栏从48px宽变成30%宽,不再“啪”一下跳过去,而是像抹了油一样顺滑。这全靠grid-template-columnsgrid-template-rows这两个属性搭配transitionanimation来实现。

侧边栏展开

拿最常见的两列布局举例。左边导航栏窄窄一条,鼠标一怼上去,立马撑开显示完整文字。以前得写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兜底。不过话说回来,这年头谁还抱着老掉牙版本不放?

浏览器最低版本支持情况
Chrome105完全支持
Firefox118完全支持
Safari15.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有个更花哨的例子——做一个响应式的动态网格,鼠标悬浮时额外弹出一列,里面塞满各种小彩蛋。原理一模一样,就是0fr1fr的过渡,再加上一点内容动画点缀。

最后补个冷知识:grid-template-rows的动画跟列完全对称。比如做一个手风琴式的行折叠效果,把某一行高度设为0fr,hover时变成1fr,照样丝滑。唯一要注意的是行内内容溢出问题,配合overflow: hiddenmin-height: 0一起食用更佳。