CSS

inline inline-block block
非块方向margin(通常是左右margin) 支持 支持 支持
块方向margin(通常是上下margin) 不支持 支持 支持
padding 支持 支持 支持
padding会影响行的高度[fn:padding与行高] 不适用
width, height 不支持 支持 支持
与其他元素相邻[fn:inline-block与换行]
transform 不支持 支持 支持

inline元素内只能存在inline元素, 如果在inline元素里存在block元素, 则会导致意料之外的渲染结果:

  • inline元素的响应范围超出实际的盒模型, 常见于a元素是block元素的父元素的情况.

以下方法只适合用于单行文本的垂直对齐.

vertical-align指的是元素和并行的其他元素在垂直方向上的对齐, 而不是元素内部的对齐方式.
换言之, 不存在text-align在垂直方向上的等价物.

<!-- 这种结构不能正确实现垂直对齐 -->
<label>
<input type="checkbox" />
text
</label>
<!-- 能够正确垂直对齐的结构 -->
<label>
<input type="checkbox" />
<span>text</span>
</label>
<style>
/* 必须在input和span上都设置vertical-align, 因为此属性不能继承 */
input, span {
vertical-align: middle;
}
</style>
<label>
<input type="checkbox" />
text
</label>
<style>
label {
display: inline-flex;
align-items: center;
}
</style>

多行文本的垂直对齐依赖于将被对齐元素(inline-block)的高度(height)与行高(line-height)匹配
(可能需要手动设置 vertical-align: bottomvertical-align: top), 然后在内部使用inline-flex做居中.

另一种方式是创建脱离文本流的伪元素, 再手动调整偏移量.

使用em单位设置SVG图标的大小与行高一致.

浏览器的亚像素处理算法是缺乏规范的, 不同的浏览器有不同的舍入和渲染方式.
在使用em, rem这样的相对单位时, 浏览器计算出来的px很可能不是整数,
进而在一些显示设备上出现亚像素渲染问题.

Firefox(v91.0.1)和Chrome(v92.0.4515.159)在border-radius的亚像素渲染上都存在变形的问题.

样式的优先级与选择器的特异性有关, 选择器的特异性越高, 则样式的优先级越高.

选择器特异性按以下顺序从低到高排列:

  1. 1.
    类型选择器, 伪元素选择器
  2. 2.
    类选择器, 属性选择器, 伪类
  3. 3.
    id选择器

当样式使用 !important 时, 会破坏优先级规则, 强制使用相关的样式.

类名顺序不会影响样式的优先级, 后添加的类名不一定会覆盖之前的类名.

通过 max-width: 100%, 图片将永远不会放大到超过其原始尺寸.

img {
max-width: 100%;
height: auto;
}

为img添加 max-height: 100%.

有别于将长宽中的一个设置为auto的等比缩放, 固定纵横比缩放的指的是由用户指定纵横比的缩放,
这种缩放不受资源本身的纵横比影响.

典型的使用场景是缩放像iframe这样的不具有比例的元素.

https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-ratio
https://www.bram.us/2020/11/30/native-aspect-ratio-boxes-in-css-thanks-to-aspect-ratio/

https://www.bram.us/2017/06/16/aspect-ratios-in-css-are-a-hack/

使用vertical padding进行固定纵横比缩放是一个历史悠久的著名CSS hack,
它最终会被CSS4的 aspect-ratio 属性替代.

实施vertical padding的关键在于将盒子的高度设置为0,
然后用以宽度为比例计算的垂直padding来撑起盒子的高度.

按4:3缩放背景图片:

.ratio4-3 {
background-image: url(image.svg);
background-size: cover;
width: 100%;
height: 0;
padding: 0; /* reset padding */
padding-bottom: calc(3 / 4 * 100%);
}

当元素不是 position: static 时, 元素会响应top, right, bottom, left, z-index属性.

默认值, 元素处于文档布局流内.

元素处于文档布局流内, 相对其原本的位置进行偏移.

元素脱离文档布局流, 向上查找找到最近的不是 position: static 的祖先元素, 相对其位置进行定位.
如果没有找到这样的祖先元素, 就会选取html.

在不设定top, right, bottom, left的情况下, 元素会处于其原本在文档布局流的位置.

元素脱离文档布局流, 相对于视口(viewport)进行偏移.

表现与static相同, 但在页面向下滚动导致元素超出视口(viewport)可见范围时, 变成fixed.

margin collapse指两个相邻元素之间的margin被渲染引擎合并为其中更大的那个margin.

会出现margin collapse的情况:

  • 相邻元素
  • 完全空白的盒子
  • 父元素和第一个及最后一个子元素

不会出现margin collapse的情况:

  • position: absolute
  • 元素是Flexbox或Grid容器及其子元素

margin和padding的取舍关键在于属性对布局的影响(在 box-sizing: border-box 的情况下):
margin是盒模型"盒子外部"的一项属性, 这导致margin会使元素与其他元素拉开距离.
padding是盒模型"盒子内部"的一项属性, 使用padding不会对元素所处的布局有任何影响, 因此可以让元素布局效果更清晰.

由于margin collapse机制的存在, margin适用于需要折叠的情况, padding适用于不需要折叠的情况.

max-content 内容不会溢出的最大尺寸.
min-content 内容不会溢出的最小尺寸.
fit-content是max-content和min-content的结合体, 当空间足够时, 会选择max-content, 反之选择min-content.

::marker 在不同浏览器上的显示效果不同, 例如 list-style-type: disc; 在Chrome上有margin, 但Firefox上没有, 并且两者显示的图案也是有明显差异的.

::marker 最大的问题在于它目前允许使用的CSS属性少得可怜, 以至于无法通过CSS对样式进行归一化.

list-style-image在多大多数场合下都不如组合使用 ::beforebackground-image, 因为它的浏览器渲染.

显示出的::marker宽度为0.

content-box 默认值, 只有content会被算进盒模型的长宽, 因此在 width: 100% 时设置padding或border会导致盒子的尺寸超出width的设置.
border-box 常用值, 会将padding和border算进盒模型的长宽, 因此在 width: 100% 时设置padding和border, 盒子的尺寸仍然遵从width的设置.

auto意味着扩展至其父元素下的剩余空间, 尊重broder, padding.
100%意味着使元素与父元素一样的宽度, 不尊重border, padding.

通过为子元素设置 box-sizing: border-box, 可让100%具有和auto类似的行为.
100%可以处理一些auto无法实现的填充情况, 例如 display: inline-block 的auto就可能无法填充.

子元素的 height: auto 通常不会有用.

将子元素的height设置为与父元素相同值方法只有 height: 100%,
但这要求父元素有一个准确的高度.

如果需要在父元素没有准确高度的情况下让子元素符合父元素的高度,
只能将父元素设为 display: flexdisplay: inline-flex.

一维布局解决方案.

display: flex

flex元素及其子项默认具有 min-width: automin-height: auto.
参考: https://www.w3.org/TR/css-flexbox-1/#min-size-auto

这些默认值会导致部分元素无法按符合直觉的方式缩小, 其造成的结果就是元素溢出.
Chrome浏览器在2017年将部分元素的实际表现修改为了 min-width: 0; min-height: 0 的效果,
但Firefox并没有跟进这项修改, 这导致Firefox上更容易出现元素无法按理想方式收缩的情况.
为解决此问题, 建议将元素重置为 min-width: 0.
参考: https://stackoverflow.com/questions/36247140/why-dont-flex-items-shrink-past-content-size

为了更符合直觉, 开发人员可能会考虑将所有元素的min-width和min-height重置为0, 但这 不安全:
当元素的宽度可能被收缩得比min-content要小时, 文本会溢出显示, 因此错误样式可能在意想不到的地方出现,
为此需要再将它们重置为 min-width: min-content, 这样的话还不如只重置需要重置的元素.

  • form
  • input
  • 嵌套了响应式img的容器

flexflex-grow, flex-shrink, flex-basis 的简写属性.

flex: initial 相当于 flex: 0 1 auto
flex: auto 相当于 flex: 1 1 auto
flex: none 元素既不扩张也不收缩.

元素的扩张因子, 默认值为0(不会扩张).

元素的收缩因子, 默认值为1(会收缩).
0 不收缩

元素收缩能达到的最小大小是 min-content.

元素的理想大小, 默认值为auto.

理想大小只是一个参考值,
浏览器会在无法按理想大小显示时, 根据flex-grow和flex-shrink改变大小.

flex-flowflex-directionflex-wrap 的简写属性.
flex-flow: column wrap 相当于 flex-direction: columnflex-wrap: wrap.

决定元素布局方向和排列顺序.

  • row
  • row-reverse
  • column
  • column-reverse

决定元素是否在溢出时换行, 默认值为 nowrap 不换行.

  • nowrap
  • wrap

justify开头的CSS属性都与主轴有关, 默认(=flex-direction: row=)的主轴是x轴, 也就是横轴.
align开头的CSS属性都与侧轴有关, 默认的侧轴是y轴, 也就是竖轴.

place-contentalign-contentjustify-content 的缩写.

主轴上存在多组元素时的空间分布.
flex-start, 默认值, 元素优先排在容器头部, 空白留在容器尾部.
flex-end, 元素优先排在容器尾部, 空白留在容器头部.
center, 元素优先排在中间, 空白留在容器头尾.
space-between, 均匀分布元素, 将空白留在元素与元素之间的空隙.
space-around, 均匀分布元素, 将空白留在元素与容器头部, 元素与容器尾部, 元素与元素之间.
容器头尾部分的空白比元素之间的空白要小.
space-evenly, 均匀分布元素, 类似于 space-around, 但头尾部分的空白与元素之间的空白更接近.

类似于 align-content, 但该属性将侧轴同一条线上的元素视作一组(例如一行或一列),
而不是将所有元素视作一组.

在默认情况下, 该值为 normal, 会将元素沿着侧轴拉伸.
典型的横向布局时, 会将所有元素的高度拉伸到最高, 通过 flex-start 取消此行为.

一般语境下的侧轴对齐是 align-items, 而不是此属性.
侧轴上存在多行元素时 的空间分布, 大部分值与 justify-content 一样, 只是作用在侧轴上.

stretch, 拉伸元素的侧轴尺寸, 从而占满侧轴空间.

单个元素的侧轴对齐, 该属性设置在具体元素而不是容器上.

Flexbox和Grid不需要使用margin创造间隙, gap更好用.

gaprow-gapcolumn-gap 的简写.

二维布局解决方案.

display: grid

在Chrome的开发者工具中的layout选项卡里, 可以开启与Grid有关的辅助功能, 例如显示行号和列号.

Grid使用与Flexbox相同的分布, 对齐, 空隙属性.

通过游戏学习它: http://cssgridgarden.com/

  • 页面整体布局
  • 二维的流式布局
  • 行/列子元素数量固定情况下的固定列数均分网格布局, 此时实际上是在模拟设计语境下的网格, 正确的用例很少见, 据我所知只有:
    • 显示键盘键位
    • 显示元素周期表
    • 显示表格(例如日历)

line: 网格由line组成, line有垂直和水平两种.
track: 连续的两条line之间的空间被称为track, 相当于人们熟知的"行"和"列".
ceil: 垂直与水平方向两条连续的线之间切出的空间被称为ceil, 即单元格.
area: 多个单元格组成的区域.
gaps: 单元格与单元格之间的空隙.

  • fit-content(wrap_value) 定义 fit-content, 但以 wrap_value 作为判断是否溢出的标准.
  • minmax(min_value, max_value) 定义track的最小和最大大小, 从而使网格具有弹性.
  • repeat(times, value) 将 value 重复 times 次, 其中 value 可以是多个值.
  • repeat(auto-fill, value) 流式网格布局, 尽可能多的定义行/列, 每一行/列的尺寸为 value, 如果行/列容纳不下, 则会换行/列.
    如果在 value 使用 minmax(), 则会出现类似于响应式设计的效果.
  • repeat(auto-fit, value) 与auto-fill类似, 但在只有一行/一列的情况下, auto-fit的单元格会自动拉伸以填满剩余空间.

grid-templategrid-template-rows, grid-template-columns, grid-template-areas 的简写形式.

.container {
display: grid;
grid-template:
"head head head" minmax(150px, auto)
"sidebar content content" auto
"sidebar footer footer" auto / 1fr 1fr 1fr;
}

这是一个多值属性, 每一个值代表一列的尺寸, 值可以是任何长度单位, 并且单位可以混用, 值的数量会决定网格的列数.

这是一个多值属性, 每一个值代表一行的尺寸, 值可以是任何长度单位, 并且单位可以混用, 值的数量会决定网格的行数.

这两个属性决定了当没有定义track的尺寸时, 使用的默认尺寸.

改变元素的放置方法, 默认值为 row.

第一参数:
row 元素先从左到右, 再从上到下依次成为单元格.
column 元素先从上到下, 再从左到右依次成为单元格.

第二参数:
dense 当由于元素换行导致出现空白区域时, 试图用后面大小相符的元素来填补空白.

该属性与 grid-area 命名的元素配合使用, 可明确指定它在网格中的位置.
每一个字符串都是一行, 重复的名称会使元素跨越track.
由特殊值 . 组成的名称用于将对应的单元格填充为空白.

.container {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-areas:
"....... header header header"
"sidebar content content content"
"sidebar footer footer footer";
}

grid-area 有两种值类型:

  • 网格直接子代的名称(推荐).
  • grid-row-start, grid-column-start, grid-row-end, grid-column-end 的缩写.

如果一个单元格既没有 grid-area, 也没有 grid-columngird-row, 它会按照 order 和HTML元素的顺序依次摆放.

grid-columngrid-column-startgrid-column-end 的缩写.
grid-rowgrid-row-startgrid-row-end 的缩写.

属性的值需要用 / 分隔开, 示例:
auto / span 2

设置行和列的起始位置(指定线的序号或名称).
默认值为 auto.

设置行和列的结束位置(指定线的序号或名称), 可以为负值, 代表倒数的第x条line.

grid-column-endgrid-row-end 在使用 span 时可以相对start跨越多条track, 示例:
span 2 相当当前start跨越两条track

有符号整数, 默认为 0, 代表子元素按顺序摆放时的顺序.

grid 是以下属性的简写, 太复杂所以不建议使用:

  • grid-template-rows
  • grid-template-columns
  • grid-template-areas
  • grid-auto-rows
  • grid-auto-columns
  • grid-auto-flow

[name] 标识line的名称, 可用空格来给line设置多个名称.
通常会使用start和end作为线名称的修饰符, 尽管可以偷懒使用不带有修饰符的名称, 但出于可读性考虑不推荐这么做.

属性值定义的是 line的名称track的大小:

.container {
display: grid;
grid-template-columns: [sidebar-start] 1fr [sidebar-end content-start] 2fr [content-end];
}
.content {
grid-column: content-start / content-end;
}

fr单位之所以比%和vw更好, 是因为fr自动计算占据的百分比, 而不是由用户手动表示.
例如, 1fr 2fr相当于33% 66%, 我们无需手动计算比例.

CSS网格很容易被认为是对设计里的网格系统的实现, 这是错的:
网格系统将界面均等划分为十几列, 每个元素的具体位置会通过网格系统进行描述, 例如从第n列开始, 横跨m列.
相比之下, CSS网格的尺寸是弹性的, 不需要也不应该使用这种网格系统.

/* BAD */
/* 均分为14列的网格系统, 子元素的位置会缺乏可读性和可维护性 */
grid-template-columns: repeat(14, 1fr);
/* GOOD */
/* 不受具体列数限制的网格设计 */
grid-template-columns: [left-gutter] 1fr [sidebar] 4fr [content] 8fr [right-gutter] 1fr;

原生的多列功能主要用于文本的多列显示.
如果将其用作布局, 处于一列尾部的内容可能被破坏, 导致其溢出到下一列的首部, 为了避免这种中断, 需要设置 break-inside: avoid.

该属性是 column-countcolumn-width 的缩写, 两种值的先后顺序是任意的, 也可以省略其中一个值.

列的数量.

列的宽度, 值为 auto 时意为由其他属性决定列的宽度.

列之间的空隙, 该值也被用于支持Flexbox和Grid的列间隙.

column-span: all 使子元素强制跨列显示.

列之间的线的样式.

参考: https://css-tricks.com/piecing-together-approaches-for-a-css-masonry-layout/

水平砌体布局显示起来像砖块堆成的墙.

Flexbox非常适合实现水平砌体, 只需要设置 flex: wrap 就能实现.
为了让"砖块"两端对齐, 还需要加上 justify-content: space-between.

启用两端对齐后, 尾行内容如果不能填满整行, 就会出现比较差的两端对齐效果.
为了解决这个问题, 需要在flex容器最后添加一个不可见的隐藏元素, 这可以用 ::after 伪元素来实现.
为了让隐藏元素撑开空间, 需要将它的flex-grow设置为一个较大的值, 比如 flex-grow: 9999.

  • https://github.com/desandro/masonry (推荐使用colcade替代它)
  • https://github.com/desandro/colcade
  • https://github.com/e-oj/Magic-Grid

可以使用CSS多列来实现垂直砌体布局, 实现起来非常简单.

CSS多列的子元素是先放满一列, 然后再放进下一列, 这种顺序通常不是人们想要通过砌体布局实现的效果.

有一种基于CSS多列的方案是把子元素重新排序后再显示, 这样就能以相对正确的顺序显示.
然而这不适合绝大多数使用砌体布局的场景: 列数随页面宽度改变, 向下滚动页面会加载更多的子元素.

Flexbox实现垂直砌体布局的方式和CSS多列基本一样, 所以也 存在相同的顺序缺陷.

https://tobiasahlin.com/blog/masonry-with-css/

当没有用 grid-template-rows 显式定义行轨道模板的时候, Grid会自动构建隐式的行轨道.
在这种情况下, 为单元格设置带有 spangrid-row-end, 会产生类似砌体布局的效果, 并且元素的布局顺序是正确的.
运用这种特性, 可以用JavaScript计算单元格的 grid-row-end 值, 从而实现砌体布局.

参考:

  • https://w3bits.com/css-grid-masonry/
  • https://medium.com/@andybarefoot/a-masonry-style-layout-using-css-grid-8c663d355ebb

当前有一个让Grid原生支持砌体布局的草案, 为 grid-template-rows 支持特殊值 masonry:

  • https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout/Masonry_Layout
  • https://www.smashingmagazine.com/native-css-masonry-layout-css-grid/
  • https://drafts.csswg.org/css-grid-3/

使用Canvas实现的图表库.

HTML的类名是固定的, CSS通过这些固定的类名为页面添加样式.

例子:

  • CSS Zen Garden.

CSS的类名是固定的, HTML通过添加类名为页面添加样式.

例子:

  • Bootstrap
  • Bulma
  • Tailwind
  • Animate.css

HTML的元素名的语义含糊不清, 最终带来混乱局面和意外的样式, 请只使用类名.

内联样式是最脆弱最难维护的样式, 禁止它们.

这比禁止使用内联样式更进一步, 在编写类的样式时也禁止硬编码, 使用CSS变量, 或将原子化CSS混入当前样式.
通过避免编写"真实的CSS样式", 我们创建出了统一的"抽象层".
这可以和设计领域的"设计系统"很好地融合起来.

使用语义化的类名(例如title), 而不是描述样式的类名(例如align-center).

BEM定义了一种语义化类名:
block--modifier
block--modifier-value
block__element--modifier
block__element--modifier-value

特点:

  • 模块化, 每个组件只有一个block类名, 不同block不会重叠在一个组件上.
    每个组件下有该组件特有的element类名, 不同element不会重叠在一个元素上.
  • 类名是语义化的, 有结构的.
  • block与element仅仅通过语义建立联系, 不使用CSS选择器的层级结构.

示例:

<!-- block: author-bio -->
<div class="author-bio">
<!-- element: name -->
<!-- modifier: color -->
<!-- modifier value: yellow -->
<h2 class="author-bio__name author-bio__name--color-yellow"></h2>
<!-- element: avatar -->
<h2 class="author-bio__avatar"></h2>
</div>
.author-bio {}
.author-bio__name {}
.author-bio__name--color-yellow {}
.author-bio__name--color-red {}
.author-bio__name--color-green {}
.author-bio__avatar {}

在团队中讨论决定语义化类名很可能是一个痛苦的过程.

原子化CSS使用原子化的类名, 由于没有高级的, 抽象的类名, 于是无需决定命名.

当我们需要创建一个与原组件在设计上相似但又有决定性不同的新组件时,
由于语义化的类名, 我们需要创建一个新的组件名, 接着复制原组件的所有CSS类名将其改为新的组件名.
我们接下来需要维护这两套内容极为相似的CSS类名, 两套组件的一部分会各自发展, 但又有一部分是共通的,
这会发展成为一个维护问题.

这种关系很像OOP里的继承, 因此我们可以通过提取出多个组件的共同部分形成一个基类来解决问题(@extend).
我们认为继承可能导致子类激增(类名会变得超级长),
于是我们将样式拆解成更小可重用单位以组合出我们需要的样式(@mixin).

"组合优于继承"的哲学使得我们意识到我们需要复用更小的样式单位,
这实际上就是原子化CSS(Atomic CSS).

这条路径最终引出基于设计系统的可重用原子CSS类名方案: Tailwind.

这将使得那些原本看起来很具体的类名变成泛化的类名, 例如article变成media-card, 后者是泛化的.

这种方案的缺点在于泛化的类名与不同类型的组件混合在一起.
泛化的类名可能有很多变体, 最终变得难以管理,
它使用倒退的方式解决问题, 因此在使用语义化类名之前的CSS问题将卷土重来.
此外, 当泛化的类名需要进一步泛化时, 又该怎么办? 这显然不是一种合理的解决方案.

一个元素会继承它父组件的样式, 因此当CSS样式变得很多且分散的时候, 推理元素依赖的样式变得非常困难.
这种元素与样式, 样式与样式之间遥远的认知距离使得开发人员需要频繁从浏览器的开发人员工具中寻求帮助,
并且在修改样式时更倾向于"仅追加(append only)"的编码风格,
这使得旧样式难以被删除(会破坏那些隐式依赖它的组件),
最终进一步推高样式的数量, 形成恶性循环.

CSS原子化的成功之处在于CSS样式到具体的元素的认知距离非常近, 开发人员不必担心隐式的样式依赖.

CSS in JS方案大多遵循语义化类名思想, 但以不同的方式实现.
CSS in JS会生成随机的类名, 类名只是用于绑定到具体组件的桥梁, 本身没有价值.
CSS in JS很接近内联样式, 样式与组件紧密联系在一起, 而不是像传统CSS模块化方案那样独立编写HTML和CSS.

这种方案直接解决了"仅追加样式"的问题, 因为组件需要在多种任何场合可用,
因此组件必须在设计时就避免被外部事实来源改变(例如某个全局CSS文件),
只留下少数可定制的props(或者CSS变量).

它们使用了一种不遵循语义化类名思想的CSS in JS方案:
让组件样式具有隔离性, 以至于组件样式不会泄漏到其他组件上.

在这种组件里可以直接编写"真正的CSS"(可以用CSS选择器而不是只用类名).
由于组件的代码规模通常不会太大, CSS代码得以能够维护.

使用浏览器的Shadow DOM进行隔离.

为元素添加随机的属性, 在编译CSS时为元素选择器添加相关的属性选择器.

无论是模拟内联样式还是具有隔离性的CSS, 都没有消除上述问题的认知距离, 只是将认知距离缩小到可控范围.

  • 一些CSS in JS实现(例如styled-components)依赖于动态解析CSS, 性能比原生CSS的性能差.
  • 组件助长了通过自定义属性/CSS变量来隐藏具体的内部样式的模式:
    虽然从OOP的角度来看, 隐藏内部细节经常是一件好事,
    但这对样式这种具有一套公共语言(CSS)的属性集来说则恰好相反.
    这种模式是现代组件库缺乏定制性的罪魁祸首.
    renderProp和Headless组件因此走向了CSS in JS的反面: 抛弃向上层暴露样式接口的做法.

原子化CSS是一种实用工具库, 但它又不仅仅是实用工具库, 它实际上是一种在CSS里形成的语言/共识,
因为在使用原子化CSS之后, 除非必要, 不应该编写"真正的CSS".

在Tailwind的官方文档里, 不提倡把多个原子化CSS提取成语义化类名, 而是提倡用JavaScript把它们提取成组件或模板.

页面样式高度统一化, 极少出现独特样式的网站.

  • 不需要浪费时间发明类名和达成共识.
  • 不再出现"仅追加样式"的情况.
  • 设计统一性.
  • CSS文件明显小于其他方案

由于原子化CSS大量使用类名, HTML文件会明显比其他方案要大.

原子化CSS通常是一种建立在类名之上的语言, 在使用之前需要先学习这些语言.

原子化CSS在适应新的CSS属性上存在困难, 尤其是对于那些值很复杂的CSS属性而言.
例如, Tailwind就没有支持 grid-template-areas, 只支持transform的 rotate 而不支持 rotateXrotateY.

一些复杂的属性值是难以扩展的.
拿Tailwind的transform举例, 框架使用了自定义的CSS变量来实现此属性值:
transform: translateX(var(--tw-translate-x)) ...
当用户需要自己添加新的属性值时, 自定义的tranform值会覆盖掉框架原先设定的值, 导致相关的类名失效.

像Tailwind这样的框架无法直接表达 calc(100vh - 4rem).
若想使用此值, 就需要依赖于预先定义好的配置,
这可能会导致实际的值与设置该值的地方距离过远, 带来认知负担.
为这些具有独特意义的复杂值命名也很困难.

在原子化CSS框架里, 通常只有过渡动画和几种预设的动画能够使用, 所有复杂的动画都需要自定义CSS类.
这是因为CSS动画经常涉及到多个元素, 多个属性在多个关键帧的变化.
此外, 将专用于动画的大量CSS类写进HTML里还会严重损害代码可读性.

由于缺乏高级抽象, 原子化CSS的可移植性存在疑问, 不同的原子化CSS库可能使用完全不同的原子类名.

由于缺乏语义, 并且添加了大量类名, 源代码的可读性会变差.

一些原子CSS框架完全避免抽象(例如Tailwind), 因此在使用时需要为每个元素手动指定样式.
在没有组件的情况下, 这会导致相当多的重复编写.

有一些CSS属性是无法继承的, 比如vertical-align.
这时, 如果需要子元素全部实现垂直对齐, 就需要给每一个子元素添加相关类名, 这种重复是不可接受的.

原子化CSS库可能会犯错误, 关键在于原子化CSS库的错误对使用库的开发人员来说是不可修复的,
开发人员只能等待维护者修复问题.

如果维护者认为这不是错误而是特性, 或者这已经是一个被广泛使用的错误实现, 因此无法修改,
那么开发人员将面临以下问题:

  • 很难快速迁移到其他库
  • 不得不迁就错误实现
  • 在原子化CSS之上实现自己的分支

使用量最大的预处理器.
最早用Ruby编写, 后来用Dart重写.

Bootstrap 4从Less切换到了Sass.

使用量第二大的预处理器.
Less晚于Sass推出, 但由于是通过JavaScript编写的, 更容易与前端项目集成, 赢得了一批红利.

Less的一些设计与现有的CSS语法容易混淆, 并且相比Sass和Stylus的功能更少.

使用量第三大的预处理器, 由TJ开发.

Stylus的主要特点是它的语法非常宽松.

时至今日, Stylus的开发和发布并不积极.

CSS的主流后处理工具, 分析和生成原生CSS代码, 可为CSS创建自定义语法.

PostCSS通过大量的插件支持各种各样的功能.

PostCSS最知名的插件, 用来将CSS转换成常见渲染引擎的特有形式, 以满足跨浏览器兼容性.

将CSS的 @import 转换为单文件的内联形式.

PostCSS主要的实现主流预处理器语法的插件.

该项目上一次更新是在2018年, 不应该继续使用了, 推荐直接使用Sass.

PostCSS主要的压缩工具.

CSS动画默认情况下会立即播放, 因此在页面载入后, 当元素获得相应的 animation-name 属性时就会开始播放.
移除并重新添加 animation-name 会导致动画再次播放.

用于设置动画的播放状态, 将状态从 paused 转换至 running 时, 暂停的动画会从暂停处继续.

决定动画的关键帧如何在动画开始前或结束后影响元素.

  • none: 默认值, 不影响.
  • forwards: 动画最后一帧的样式会在动画播放结束后仍应用于元素.
  • backwards: 动画第一帧的样式会在动画播放前就应用于元素.
  • both: forwards+backwards.

动画停止播放前应该播放的次数.
默认值为1, 最常用设置的值是infinite.

决定循环动画的播放方向, 对于多个动画, 可以指定不同的值:

  • normal: 正向播放动画, 播放完毕后重置到初始状态再开始.
  • reverse: 反向播放动画, 播放完毕后重置到初始状态再开始.
  • alternate: 正向播放动画, 播放完毕后反向播放动画, 如此反复.
  • alternate-reverse: 反向播放动画, 播放完毕后正向播放动画, 如此反复.

追求纯CSS的特效通常没有意义, 因为它们大多缺乏可维护性, 很难移植, 令人生畏.
追求酷炫的特效通常没有意义, 因为这些效果缺乏实用目的, 经常被纯粹的平面设计方案击败.
绝大部分循环播放的动画效果是没有意义的, 因为它们会分散用户的注意力.

网络上大部分的CSS例子都很愚蠢,
其中至少有80%的作品是由缺乏对可维护性的认识的作者, 在对属性的详细情况缺乏了解的情况下,
通过多次尝试和实验做出来的, 因此充满了冗余/不必要/可读性不佳的CSS.

和React面临的问题类似, 如果不学习CSS的历史, 初学者将很难分辨例子使用的方法是否足够"现代".

  • animate.css
  • anime.js
  • lottie-web: 从After Effects导出特效
  • https://tobiasahlin.com/spinkit/

这个方法的优点是不需要JavaScript, 开启状态可以用选择器 details[open] 表示.

由于没有JavaScript, 多个details不能连动, 因此可以同时展开多个details元素.

设置一个不显示的checkbox(input[type="checkbox"] { display: none }),
然后创建一个绑定到该checkbox的label元素.
利用点击label等同于点击checkbox的特点, 把label作为可自定义外观的checkbox使用.

checkbox选中状态可以通过选择器 =input[type=checkbox]:checked= 表示,
利用这一点, 使用通用兄弟组合器 ~ 选择后续的兄弟节点, 将兄弟节点展开.

  • 比details + summary繁琐太多了, 属于没有details和summary年代的CSS hack
  • 由于没有JavaScript, 展开的元素之间不能连动.

实现方式与用隐形checkbox实现手风琴类似, 只不过将checkbox换成了radio.

值得一提的是, 纯CSS轮播的"上一个", "下一个"按钮的实现方式很有创意:
由于不能使用JavaScript, 所以不能基于状态复用"上一个", "下一个"按钮,
于是需要在每一个页面上创建与该页面相关的"上一个"和"下一个"的label按钮, 这些label只有在相应的radio处于checked状态时才会显示.

没有任何理由去推荐使用这种无JavaScript的轮播组件, 因为它们的实现是基于CSS hack, 代码非常不直观.

纯CSS形状是早年的CSS hack手法, 并不推荐使用, 因为编写出来的CSS很不直观.

参考: https://css-tricks.com/the-shapes-of-css/

clip-path通过裁切元素来获得形状, 它支持许多基本形状和路径, 后者可以用来制作任何形状.

使用时, 建议找一个专门的clip-path编辑器来可视化裁切结果.

https://codepen.io/stoumann/full/abZxoOM

SVG图标近年来已经成为最流行的选择, 它最适合用来制作各种形状.

描边动画的关键:

  • stroke-dasharray: 在描边中创建空白虚线, 如果虚线超过整个描边的长度, 则整个描边都会显示为空白.
  • stroke-dashoffset: 创建偏移, 用于搭配 transform 创建过渡动画.

参考: https://css-tricks.com/svg-line-animation-works/
库: https://github.com/maxwellito/vivus

该属性/函数相当于CSS 3D效果的强度, 值越小, 3D效果越强烈.
perspective属性会影响到它的直接子元素.

参考: https://3dtransforms.desandro.com/perspective

在同一个3D渲染上下文中的物件才会相交, 进而在显示时出现一个物件靠前的部分遮挡另一个物件靠后部分的效果.
不在同一个3D渲染上下文中的物件位于不同的3D空间, 物件不会相交, 最终显示结果是上下文各自渲染完毕后, 合成两者的2D图像.

参考: https://css-tricks.com/things-watch-working-css-3d/

  • flat 默认: 元素的子元素不会使用相同的3D渲染上下文.
  • preserve-3d: 元素的子元素将使用相同的3D渲染上下文.

实现方式是在一个3D空间内创建两个大小相同的卡片.

<!DOCTYPE html>
<html>
<head>
<style>
.card {
width: 150px;
height: 200px;
transition: transform 1s;
transform-style: preserve-3d; /* 使子元素位于同一个3D空间, 能够互相遮挡 */
}
.card--flipped {
transform: rotateY(180deg); /* 将整张卡绕Y轴旋转180度, 相当于把卡翻过来 */
}
.card__face {
width: 100%;
height: 100%;
position: absolute; /* 使两张卡片重叠 */
}
.card__face--front {
background-color: brown;
}
.card__face--back {
background-color: darkcyan;
transform: rotateY(180deg); /* 将这一面绕Y轴旋转180度, 作为卡背 */
}
</style>
</head>
<body>
<div class="card">
<div class="card__face card__face--front">front</div>
<div class="card__face card__face--back">back</div>
</div>
<script>
const card = document.querySelector('.card')
card.addEventListener('click', () => card.classList.toggle('card--flipped'))
</script>
</body>
</html>

参考: https://3dtransforms.desandro.com/card-flip

  • https://3dtransforms.desandro.com/cube
  • https://codepen.io/desandro/pen/KRWjzm
  • https://3dtransforms.desandro.com/box
  • https://codepen.io/desandro/pen/MGpMOV

视差效果是通过将元素分层, 然后让位于不同层的元素以不同的速率位移, 从而形成的独特视觉效果.
展示视差效果时, "靠得越近"的事物移动的速度越快, "离得越远"的事物移动的速度越慢.

  • https://github.com/janpaepke/ScrollMagic
  • https://github.com/locomotivemtl/locomotive-scroll
  • https://github.com/alvarotrigo/fullPage.js

视差滚动是与页面滚动相关的视差效果.

实现性能较好的视差滚动, 关键在于使用 perspective (在父元素上) 和 translateZ() (在子元素上).
translateZ() 调整Z轴, 正值让物体离观看者更近, 负值离观看者更远.
perspective 调整透视, 从而影响不同Z轴距离内容的滚动速度.

遗憾的是, 这两个函数配合工作的方式很不直观, 因此很难搞清楚应该采用什么值来达到想要的效果.
一种惯用的方案是设置 perspective: 1px, 然后以 -1px 为步长逐步减小 translateZ() 的值.
为了让元素不至于显得过小, 会用 scale() 进行补偿,

用以下公式可以计算出放大回原始大小需要的scale:
scaleFactor = (perspective — distance) / perspective
其中 distancetranslateZ 的值, 举例来说, 当 translateZ(-1px) 时,
scaleFactor = (1 + 1) / 1 = 2.

参考: https://developers.google.com/web/updates/2016/12/performant-parallaxing

纯CSS实现的方式是将容器逆时针旋转90度, 再将容器内的元素顺时针旋转90度(即恢复到容器旋转前的方向),
从而将容器的垂直滚动条强行变成"水平滚动条"(语义上是垂直滚动条, 响应用户滚轮的向下滚动, 但视觉上的变化是水平的).

边框为渐变色的幽灵按钮, 通过 border-image-source + liner-graident() 实现.

配合scroll事件或requestAnimationFrame回调实时计算元素的top值(元素距离视口顶端的距离),
当top值小于 window.innerHeight (文档可见范围的高度) 时, 意味着元素进入视线.

通过IntersectionObserver观察元素,
仅当元素进入视口时(entry.isIntersecting 为真), 向元素添加对应的class.

参考: https://dev.to/ljcdev/introduction-to-scroll-animations-with-intersection-observer-d05

尽管 text-decoration 可以设置文本下划线, 但这种下划线无法制作任何动画效果, 因此需要用其他方式实现下划线.

box-shadow: 0 8px 0 0 #ccc

在使用inset的情况下, 阴影会置于文本的下方.
box-shadow: inset 0 -8px 0 0 #ccc

a {
text-decoration: none;
background-image: linear-gradient(red, red);
background-position: bottom;
background-size: 100% 2px;
background-repeat: no-repeat;
}
a {
text-decoration: none;
box-shadow: inset 0 -2px 0 0 #ccc;
transition: box-shadow 0.3s;
&:hover {
box-shadow: inset 0 -1em 0 0 #ccc;
}
}

box-shadow无法使用百分比单位, 因此当文字的行高大于1em时, box-shadow将无法覆盖全部的文本, 解决此问题需要为元素专门计算行高.

a {
text-decoration: none;
background-image: linear-gradient(red, red);
background-position: bottom;
background-size: 100% 2px;
background-repeat: no-repeat;
transition: background-size 0.3s;
&:hover {
background-size: 100% 100%; /* 第二个值决定了展开的高度, 出于动画目的, 此值不可省略 */
}
}

Chromium系浏览器在background-size的过渡动画中可能会出现抖动现象.
这是一个非常奇葩的bug, 经测试, 是否出现抖动现象只取决于正在动画的a元素在HTML文档里的位置, 与网页使用的CSS样式毫无关系, 并且没有规律可循.

a {
text-decoration: none;
background-image: linear-gradient(red, red);
background-position: bottom; /* 图片从中下方开始显示(从中下方展开) */
background-size: 0 2px; /* 默认情况下宽度为0, 不显示下划线 */
background-repeat: no-repeat;
transition: background-size 0.3s;
&:hover {
background-size: 100% 2px;
}
}

只需要修改 background-position, 就可以实现从不同的位置展开下划线:

  • background-position: left bottom 从左至右展开
  • background-position: right bottom 从右至左展开

单行文本大多数是被其他方法淘汰的例子, 记录它们是为了避开它们.

https://tobiasahlin.com/blog/css-trick-animating-link-underlines/

通过添加 position: absolute 的before/after伪元素作为线条,
然后设置过渡动画 transform: scaleX(0)transform: scaleX(1) 实现:
scaleX(0) 将伪元素在水平方向向中间收缩至不可见状态,
然后再通过 scaleX(1) 展开为原来的宽度.

这两种变体是通过 transform-origin (变换原点)实现的, 该属性的默认值是 center,
只要将原点该为 leftright 就可以变更收缩的位置.

https://github.com/zenozeng/fonts.css

该解决方案不能直接使用, 因为它虽然分类了各种字体, 但其中很多字体的显示效果太差, 不应该被用在页面上.

  • serif 衬线
  • sans-seirf 无衬线
  • monospace 等宽
  • cursive 花体, 草书
  • fantasy 装饰性字体
  • math 数学风格字体
  • emoji 表情符号字体
  • fangsong 仿宋体, 介于宋体和草书之间, 汉字专用
  • system-ui 与平台UI上的默认字体相同, 该值的出现使得人们不再需要手动声明系统字体
    遗憾的是Firefox不支持此值.
  • ui-serif
  • ui-sans-serif
  • ui-monospace
  • ui-rounded 环绕文本
  • 思源黑体(免费)
  • 方正悠黑(商业)
  • 苹方(macOS, iOS平台字体, 不可商用)
  • 苹黑(苹果官方网站中文字体, 不可商用)
  • 思源宋体(免费)
  • 方正悠宋(商业)
  • Titillium Web
  • Circular Std Book
  • Eudald News Regular

ch单位是字符0的宽度, 如果字体是等宽字体, 则可以通过此单位配合width决定一行文本应该包含多少个字符.

通常会将英文字体放在中文字体前面,
这是因为中文字体往往也包括英文字符, 如果放在前面会导致无法使用英文字体.

unicode-range的设计目的是为了能在下载Web Fonts时跳过那些页面里不存在相关字符的字体, 以节省带宽.
但是, unicode-range也可以通过字符集范围来为字符分别设置字体.

https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range

很奇怪, 一些字体不能以 src: local(FONT_NAME) 形式来使用, 例如 Fira Code.

https://stackoverflow.com/questions/68437455/font-face-srclocal-does-not-work-on-some-fonts

https://opentype.js.org/font-inspector.html

即使使用了web fonts, 仍有必要设置 font-display 属性(建议为swap), 以避免页面在加载字体时变成一片空白.

自托管web fonts: https://google-webfonts-helper.herokuapp.com/fonts

  • WOFF(.woff): 从TrueType和OpenType压缩而来的字体格式, 是最适合用于Web字体的格式, 所有浏览器都支持此格式
  • WOFF2(.woff2): 下一代WOFF, 使用Brotli改进压缩, 压缩率比WOFF更高, 除IE外所有浏览器都支持
  • TrueType(.ttf)/OpenType(.otf): 主流的字体格式, OpenType是TrueType的改进版本.
  • embedded-opentype(.eot): 微软提案的字体格式, 仅IE支持此格式, Edge并不支持.
  • svg(.svg): 已被弃用, 仅Safari还在支持此格式

从浏览器的市占率来看, 如今只提供woff2和woff格式的字体就够了.

@font-face {
font-family: 'Font';
src: url('font.woff2') format('woff2'),
url('font.woff') format('woff');
}

中文字符集非常巨大, 字体文件以MB级别起跳, 经常有超过10MB的字体.

WOFF2字体是可压缩的, 相比其他字体格式, 可减少30%的体积.

参考: https://web.dev/reduce-webfont-size/

可变字体往往比不可变字体体积小, 因为不需要加载额外的粗体和斜体版本.

2021年4月8日发布的新版思源黑体已经支持可变字体, 但字体大小不容乐观, 仍然在10MB以上.

对于中文字体来说, 将单个字体以不同Unicode范围切割为多个字体, 然后在使用时只下载被使用到的字体, 这种方案是很容易想象的.
对网页来说, 可以用unicode-range切分字形, 让页面只下载使用到的字符所需的字体.

由于以下原因, 此方案缺乏可行性:

  • 如何对中文字形进行切割是一个问题.
    如下方表格所示, 简体中文的常用字集有3500字, 可以覆盖99%的简体中文文本, 但是产生的字体大小对Web字体来说仍然不可接受.
    由于缺乏可以拿来作为参照的更精细的子集, 难以进一步切割.
  • 中文的常用字集在Unicode上不连续, 光是常用字集的Unicode范围就有20KB之巨.
汉字范围 字形数量 Unicode范围 字体大小(WOFF)
基本汉字 20902字 4E00-9FA5 12MB
《通用规范汉字表》(2013) 一级字表(常用字集) 3500字 不连续 1.7MB
《通用规范汉字表》(2013) 二级字表 3000字 不连续 1.7MB
《通用规范汉字表》(2013) 三级字表 1605字 不连续 976KB

参考: https://www.zhangxinxu.com/wordpress/2016/11/css-unicode-range-character-font-face/
中文的Unicode编码范围: https://www.zhangxinxu.com/study/201611/chinese-language-unicode-range.html

Python的fonttools包的一部分, 提供了很多用来定制字体子集的参数.

pyftsubset font.otf --text='你好世界' --flavor=woff
pyftsubset font.otf --text='你好世界' --flavor=woff2

https://github.com/zachleat/glyphhanger

glyphhanger利用pyftsubset实现字体子集化.
功能很丰富.
生成的文件大小优于fonteditor-core.

# 依赖
pip install fonttools
pip install brotli
pip install zopfli
  • 依赖过多
    glyphhanger是一个以CLI为主的程序, 因此打包了太多依赖.
    其中影响最大的依赖是@zachleat/spider-pig包间接依赖的puppeteer, 它会下载Chromium.
    对于精确子集化场景来说, glyphhanger的很多功能都是不必要的, 不如直接调用pyftsubset.
  • 性能损失
    glyphhanger会在转换前先根据文本文件/白名单生成一份Unicode范围表,
    然而pyftsubset实际上支持直接输入需要保留的字符, 这也更符合精确子集化的场景.
    经过测试, 直接调用pyftsubset的生成速度至少是glyphhanger的2倍.

https://github.com/harfbuzz/harfbuzz

一个用C++开发的字体整形(shaping)库, 可用来创建字体的子集.

该项目有官方的WASM版本, 可以通过JavaScript调用:
https://github.com/harfbuzz/harfbuzzjs

基于harfbuzzjs的子集模块:
https://github.com/papandreou/subset-font
经过测试, 该模块的子集化性能和生成的字体大小都与pyftsubset相当.

https://github.com/foliojs/fontkit
这是类似字蛛的库subfont使用的底层模块.

遗憾的是该库自带的subset.encodeStream方法只能输出.ttf格式的字体.

https://github.com/aui/font-spider

原理是扫描文件, 以获得所有使用该字体的CSS, 然后再从中提取到所有被使用的字符.

该项目是基于fontmin实现的.

  • 不适用于UGC网站, 因为用户会使用到的字符是不可能提前预测的, 只能加载全部字符.
  • 项目上一次更新已是2年前, 且官方网站的域名已经过期.
  • 共享fontmin所有的问题.

https://github.com/ecomfe/fontmin

字蛛的底层, 由百度EFE团队开发, 可以手动指定需要提取的字符, 然后生成字体.

只支持以ttf为源, 其他格式都要先转换为ttf再处理, 内置了一些转换插件.

该项目是的字体编辑功能是用fonteditor-core实现的.

  • 共享fonteditor-core所有的问题.

子集化所需的时间与所需的字形数量成正比.

  • 问题众多, 缺乏维护.
  • 一次把整个字体读取到内存里.
  • 对OTF的子集化实现有问题.
    https://github.com/kekee000/fonteditor-core/issues/18

通过Advanced Font Settings, 可以按ISO 15924中定义的script设置浏览器使用的默认字体.
例如, 简体中文的script是Simplified Han, 英文的script是Latin.

  • 现代网页的字体族设置方式通常是将默认字体放在最后一位作为fallback,
    页面上的文本能够使用到默认字体的情况极为罕见, 因此用户往往无法在页面上使用自己想要的字体.
  • Advanced Font Settings中设置的字体要求网页的语言与script匹配, 增加了设置的复杂性.
    在绝大多数情况下, 该扩展都不如Chrome自带的字体设置易用.
  • Advanced Font Settings使用的API Chrome.fontSettings, 曾经还有错误地将字重作为字体名提供的问题.
    虽然该问题在如今的Chrome里已经解决, 但仍然无法为默认字体配置字重.
    因此在Appoint Font里, 相关API只被用来获取字体列表.

该方案的问题在于, 除非用于重置字体的扩展程序逐一读取元素的CSS样式,
否则扩展程序无法知晓字体应该被重置为哪种类型.

重置元素样式本身作为一种对页面的侵入行为, 还可能引起降低页面性能的副作用.
这一点在将Appoint Font用于Google的部分网页时已经得到验证.

可以用font-face替换用户代理已经安装的字体名.
加上可以通过Canvas判断出字体是等宽字体还是非等宽字体, 从而替换掉相应类型的字体.

虽然没有区分适用范围最广的衬线体和非衬线体的有效手段,
所幸对于用户代理的自定义需求而言, 通常是将它们一视同仁地替换掉.

缺陷:

  • 一些字体无法作为@font-face使用.
    作为弥补, 需要通过CSS重置元素的 font-family 作为fallback.
    在Appoint Font里, 在body元素上应用了此fallback.