在这里记录我的 CSS 编码风格,不一定是最佳实践,也欢迎读者针对特定场景提供更优的写法。
前置利器:StyleLint
在遵循需要手动修复的规范之前,应当最大化利用 IDE 提供的自动修复能力。我在 NPM 上单独维护了一个包来在各个项目之间共享配置,以提供最简洁的开箱即用。
1 2 3 4
// stylelint.config.js import zin from "@zinkawaii/stylelint-config"; export default zin();
注意需要同时安装
stylelint
包,就像eslint
和@antfu/eslint-config
一样。
本文将着重讨论对于 CSS 规则的结构与组织的手动优化。
重置默认样式
必须简单粗暴,这里是为数不多可以用到通配选择器的地方,该让自己爽一爽了。
1 2 3 4 5 6 7 8 9 10
* { margin: 0; padding: 0; border: 0; font: inherit; } *, ::before, ::after { box-sizing: border-box; }
尽可能使用 grid
而不是 flex
Grid 就是现代 CSS 黑魔法,每个人都应该学会使用 Grid(混乱 ✘
子元素居中
在使用 flex
时,对子元素的居中需要同时控制其在主轴和交叉轴上的对齐方式,它们分别受 justify-content
和 align-items
属性控制,这将导致 CSS 语句达到 3 行;
而对于 grid
,则需要使用 justify-items
和 align-items
来控制子元素在水平方向和垂直方向上的对齐方式,此时这两个属性可以合并为 place-items
,CSS 语句减小到了 2 行。
1 2 3 4 5 6 7 8 9 10
.before { display: flex; justify-content: center; align-items: center; } .after { display: grid; place-items: center; }
简写属性是使 CSS 保持精简整洁的重要手段,下面也会介绍更多关于简写属性的样例。
轴向自适应
当子元素拥有确定的数量时,希望某些子元素能够自动捕获容器剩余部分的长度,常见手段往往是 flex
父容器配合指定了 flex
属性的子元素;这是否导致了某种程度上的耦合?
倘若使用 grid
,则能够将子元素的排列尺寸完全交由父元素控制。
1 2 3 4 5 6 7 8 9 10 11 12
.before { display: flex; > :nth-child(2) { flex: 1; } } .after { display: grid; grid-template-columns: auto 1fr; }
垂直等间分布
如题所示,实现这种排列方式的常见手段是垂直方向的 flex
+ gap
,由于 flex-direction
的默认值是 row
,所以需要额外将它的值指定为 column
;
而 grid
则不需要这么多废话,它是默认垂直排列的。
1 2 3 4 5 6 7 8 9 10
.before { display: flex; flex-direction: column; gap: 1rem; } .after { display: grid; gap: 1rem; }
不规则分布
拿评论区举例,一条评论可以分为头像、昵称、内容和日期等信息,而除头像外的其他信息一般顺序排列在头像右侧。使用传统的 flex
时,我们需要在头像右侧单独添加一个 <div>
来装载信息;
而使用 grid
,我们则可以利用其强大的网格布局能力来减少 DOM 嵌套。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
.before { display: flex; > .right { flex: 1; } } .after { display: grid; grid-template: "A B" "A C" "A D" / auto 1fr; }
这看似使得 CSS 更加冗长了,但对于 DOM 树的精简来说是值得的。
什么时候不这么做?
-
当网站的响应式需要利用到
flex-direction
的切换时:此时的
grid
往往需要重新指定grid-template
,不如flex
来得精简。 -
容器高度有剩余且子元素数量不确定时:
很抱歉
grid
难以胜任,它会导致子元素被多余地撑开,或是无法紧贴在某一对齐方向上。 -
单水平轴子元素没有复杂的排列需求时:
正如
grid
默认垂直排列,flex
的默认水平排列更加适合这种简单的场景。 -
……
此外关于它们的应用还有许多其他的例子,
grid
完全不如flex
的状况也偶有出现,这里仅作常见情景的分析。
复合属性与简写
这里 StyleLint 会帮我们处理好大部分工作,需要注意的是什么时候不用复合属性。
只需要用到一条子属性
这时候使用复合属性显然不够直观,应该指定具体的子属性。
1 2 3 4
.foo { background-color: aqua; mask-image: linear-gradient(to right, aqua, transparent); }
使用工具类覆盖子属性
这种情况下,再次指定复合属性的值会造成代码冗余,除非原先所有定义好的值都需要覆盖。
1 2 3 4 5 6 7
.foo { border: 1px solid transparent; &:hover { border-color: aqua; } }
这都不能算是优化,毕竟就应该这样写。
用到的子属性过少时
过少的前提是它本身存在大量的子属性,直接使用复合属性可能导致超出预期的效果,比如说 font
。
1 2 3 4 5 6 7
p { line-height: 2; &.foo { font: 1.5rem "consolas"; } }
我去,我行高动了,我不玩了。
其实也就
font
了,其他复合属性基本不继承的,该用就用。
用到的子属性过多时
震惊,为什么过多子属性反而不能复合,因为一复合真看不出在写什么。
1 2 3
.foo { background: url("/path/to/background.webp") center right / 73% 81% no-repeat fixed padding-box; }
来吧,告诉我你一眼看到的答案!
我想能够毫无压力地辨识出一共使用了哪些属性的大佬一定已经是资深前端了,你们想必很清楚这种写法虽然足够简洁,但会在修改时污染版本控制的差异视图,类似于行内对象字面量写得长长一串不换行。
当然,每个人都有自己的规范,适当地拼接一些常用的值在复合属性中,剩下的属性再单独形成规则,这是我也十分提倡的做法。复合属性的利用与否最终还是会落实到具体的规则上,不是笼统的规范能够一语道明的。
transform
分解 transform
至 rotate
/ scale
/ translate
已经是老生常谈了,值得一提的是它能够和后三者叠加,或许可以作为某种进阶的 Hack 手段。
四向边距与逻辑属性
像 margin
或 padding
这样在四个方向上均有值的属性,在指定它们各自的值时按照以下几种情况判断:
仅在一个方向上有值
此时应该单独使用该方向上的属性,因为这样毫无疑问是最精简的:
1 2 3 4 5
.foo { top: 1rem; margin-right: 1rem; padding-left: 1rem; }
在两个相对的方向上有值
此时应该使用后缀为 inline
/ block
的逻辑属性,它同样保持了精简。
1 2 3 4
.foo { inset-block: 1rem; margin-inline: 1rem 2rem; }
在两个相邻的方向上有值
逻辑属性无法胜任两个相邻方向上有值的情况,此时可以选择性地使用复合属性或两条子属性。
1 2 3 4 5 6
.foo { top: 0; right: 1rem; margin: 1rem 1rem 0 0; padding: 0 1rem 1rem 0; }
三个方向及以上均有值
此时是使用复合属性的最佳场景。
1 2 3 4 5
.foo { inset: 0; margin: 1rem auto; padding: 0 1rem 1rem; }
至于为什么不使用逻辑属性的
*-start
和*-end
,我的理解是这样的:这两个后缀被设计出来的本意是支持这些属性能够根据语言之间书写模式的不同来实现方向的自适应,然而我们在大多数情况下是没有这种需求的,此时冗长的block-start
完全不如top
来得简洁直观。难道我们真的有余力在设计好完善的 i18n 同时还提供完全逻辑化的布局吗?主播的评价是别太高效。
相对屏幕居中的复数解
50% + (-50%)
1 2 3 4 5
.foo { top: 50%; left: 50%; transform: translate(-50%); }
最常见,那么什么时候使用呢?什么时候都不使用。translate
在非整数值情况下会导致字体渲染变模糊,这能忍。而且它额外占用了一个变换槽位,如果没有 scale
这些新生属性的话想在此基础上做动画可要麻烦得多了。
还是有用的,比如说我的动画需要同时在这三个属性上做手脚。如果按照下面这种方式来操作
margin
,会由于auto
无法过渡到具体的数值而发生瞬移。
inset + margin
1 2 3 4 5 6
.foo { inset: 0; width: fit-content; height: fit-content; margin: auto; }
太对了哥,就是得同时指定 width
和 height
,不出一个 size
简写真该死啊。
实际上有两个叫
inline-size
和block-size
的属性,但正如前文所说基本用不上。
纯 CSS 代替 JS
对于不少细节处的功能与交互,我都倾向于尽量使用 CSS 实现,参考这篇文章:纯 CSS 实现滚动进度指示器。
两种悬停提示
分别对应于提示框本身可不可 hover,核心是关系选择器与 pointer-events
的结合利用。
1 2 3
:hover > &.hoverable, :hover + &:not(.hoverable) { pointer-events: auto; }
非全屏滚动视差
大道至简,只需要一行规则就能完成全屏滚动视差。
1 2 3
.foo { background-attachment: fixed; }
但是,非全屏呢?
1 2 3 4 5 6 7 8 9
.foo { height: 72vh; overflow: hidden; > .child { display: sticky; top: 0; } }
笨蛋,行不通的。overflow: hidden
会将元素变成 BFC,sticky
不再相对于窗口生效。
1 2 3 4 5
.foo { height: 144vh; margin-bottom: -72vh; clip-path: inset(0 0 50%); }
而这种方式巧妙地结合了多种属性,避免了 BFC 的形成,能够使用子元素代替 background
来实现更加丰富的效果。这也是本站主页巨幕的核心实现原理。
更多小技巧
还有一些或许可以交由 StyleLint 处理,但又存在微妙差异的细节,比如 aspect-ratio
的利用,:nth-of-type()
对 :nth-child()
的取代等等,这些更多的是对 CSS 新特性的掌握与否,就不做过多的赘述了。
评论0