发现还是发明CSS技巧,怎么找到那些隐藏的特性组合?

2,816字
12–18 分钟
in

写CSS时总感觉某些效果实现起来特别拧巴?其实很多让人眼前一亮的CSS技巧并不是凭空造出来的,更像是从规范文档的角落里“挖”出来的。就像米开朗基罗说雕塑本来就藏在石头里,创作者只是把多余的部分凿掉。CSS的特性们早就互相看对眼了,就差有人牵个线让它们一起跳舞。这篇博客聊聊怎么用递归思维、拥抱限制和极端化这些野路子,把那些八竿子打不着的CSS特性撮合到一块,搞出点纯CSS的骚操作。

目录

用递归思维给CSS特性当媒人

写代码的都知道递归这玩意儿,核心就是找个出口避免无限套娃。这种思维放到CSS里,能帮人发现看似水火不容的特性其实能配合得天衣无缝。比如一个经典问题:view-timeline能不能控制触发它自己的那个东西?这就像问“理发师能不能给自己刮胡子”,但还真有解。把view-timelineposition: fixed凑成一对,这俩货就像那种天天吵架但离了对方就活不下去的奇葩室友。下面这段代码展示了怎么让滚动触发动画纯CSS实现:

.scroll-container {
  view-timeline: --scroller;
}

.pinned-element {
  position: fixed;
  animation: fadeIn linear both;
  animation-timeline: --scroller;
}

@keyframes fadeIn {
  0% { opacity: 0; transform: translateY(20px); }
  100% { opacity: 1; transform: translateY(0); }
}

写这段逻辑的时候有个坑:position: fixed元素会脱离文档流,如果没给父容器设明确高度,滚动容器就不知道什么时候触发动画。解决办法是给.scroll-container一个height: 100vh,同时确保overflow: auto。另一个容易翻车的地方是animation-timeline在旧版Chromium内核下不认,得加-webkit-前缀保平安。

让关键帧动画互相套娃

另一个脑洞:一个关键帧动画能不能触发另一个关键帧动画?这问题像问“俄罗斯套娃能不能无限套下去”。答案是能,但需要拉上三个家伙组队:@keyframes、样式查询(@container style())、还有animation-play-state。这三者凑一块能模拟出碰撞检测这种看起来只有JS能干的事。看这个例子,鼠标悬浮时让动画暂停,同时触发另一个动画:

.box {
  animation: slide 2s infinite;
}

.box:active {
  animation-play-state: paused;
  container-type: inline-size;
}

@container style(--trigger: true) {
  .explosion {
    animation: boom 0.5s forwards;
  }
}

实际跑起来会发现一个问题:style()查询目前只有Chrome Canary支持,想在生产环境用还得等。另一个翻车点是animation-play-state设成paused后,动画重置到起始状态还是停在原地?不同浏览器行为不一致。稳妥的法子是配合animation-delay负值来控制起始位置。

拥抱“啥也没有”的边界

网页设计这行,艺术和科学的界限本来就糊得不行。很多人迷信“你不能证明一个东西不存在”,但真去较真就会发现,限制反而能逼出创意。好比写诗比写散文难,因为要押韵、要格律,但好诗就是在这种框框里蹦出来的。放到CSS里也一样:没有JavaScript,能不能搞出数独游戏?没有checkbox hack,能不能做CSS游戏?甚至以后if()函数普及了,很多老hack是不是可以扔了?某个前端老哥用纯CSS做了个井字棋,为了让电脑对手能正常下棋,用HAML生成了成千上万个:target状态。CodePen直接报错说HTML太多了,最后砍成4×4数独才跑起来。这种极限施压的过程反而让人更清楚CSS的边界在哪。

/* 模拟简单逻辑:如果宽度小于400px就变红色 */
.card {
  background: lightblue;
}

@container (max-width: 400px) {
  .card {
    background: tomato;
  }
}

容器查询这个特性刚出来的时候,有人吐槽“这不就是媒体查询换了个壳吗?”但真上手就会发现,组件级别的响应式让卡片可以自己决定怎么缩放,不用听外面的页面布局瞎指挥。写的时候注意给容器设container-type,不然查询不生效。

把老特性推到极端

有些CSS特性老了容易被人遗忘,但往死里整反而能整出新活。比如:target选择器从2000年初就存在,大多数人都只在做单页目录时用过。但有人拿它做了个纯CSS的电脑对手井字棋:用HAML渲染出几千种可能的棋盘状态,每个状态对应一个:target锚点。点击格子就跳转到新的URL hash,:target匹配到对应状态后重新绘制棋盘。这种用法下页面URL会疯狂变化,浏览器历史记录直接爆炸,后退按钮基本废掉。但这不重要,因为实验目的就是看看:target到底能扛多大压力。

/* :target 伪类实战:切换显示不同面板 */
.panel {
  display: none;
}

#panel1:target,
#panel2:target {
  display: block;
}

/* 配合 :not 实现默认面板 */
.panel:target ~ .default-panel {
  display: none;
}

另一个老古董是radio/checkbox hack,2011年就有人玩。2021年有人用Pug生成几百个radio按钮做了个4×4数独,结果Chrome devtools直接卡出无限转圈圈。这翻车现场反而让后来的人知道:CSS生成的内容别超过浏览器承受极限,不然连调试工具都救不了。

局外人视角更容易捡到宝

很多人纠结“CSS算不算编程语言”这种破事。从全栈转前端的人一开始看CSS就头疼,脑核磁共振显示搞设计和搞逻辑激活的脑区真不一样。但恰恰是这种不适感,让那些从命令式编程带过来的思维模式成了优势。比如:

CSS特性类比JS/框架概念
自定义属性Vue响应式变量
:target选择器SPA客户端路由
min()/max()函数逻辑运算模拟

有人被checkbox hack启发后,一夜之间用纯文本冒险游戏的形式做了个radio按钮版游戏。设计同事看完直接惊了,不是因为游戏多好玩,而是从没见过这种操作。这种局外人身份反而成了独门武器。

写代码时经常被问“有JS不用干嘛非要用CSS折腾?”这就好比问“写散文更容易,干嘛要写诗?”诗歌就是戴着镣铐跳舞,限制越多,蹦出来的词儿越狠。CSS的边界不是墙,是跳板。下次再遇到某个效果实现不了,别急着掏JS,先想想:有没有两个互相看不顺眼的CSS特性,能拉过来硬凑一对试试?