给CSS加把锁
看到该标题,我想你可能会感到非常的意外和好奇,“CSS怎么能加把锁呢”?其实这里所说的给CSS加把锁是指业内所说的CSS Locks(或者CSS calc Lock)。该概念是@Tim Brown在2012年提出的一个概念,该技术其主要致力于解决文本排版可读性相关的技术难度,特别是在实现一些精准的流式布局中的文本排版。在《Web技术10》中也提到过,@Mike Riethmuller、@Tim Brown和@Geoff Graham都在致力于这方面的研究,而且找到了使用CSS锁技术方案来锁定流式排版。让我们实现一个精准的流式排版不再是难题。在今天这篇文章,我们来一起探讨一下CSS锁的故事。
什么是CSS锁
这里所指的CSS锁并非说给CSS加把锁,让程序无法写入CSS代码,所指的是CSS Locks。那么具体什么是CSS锁呢?如果要搞清这个概念,我们要先回顾一点Web相关的知识。
响应式设计
早在2010年@Ethan Marcotte就提出了Web响应式设计(Responsive Web Design)的概念。“响应式设计”简单地说是可以让你的网站从宽屏显示器到手持移动终端(比如手机)都能良好的显式。
这是一种Web设计和开发的方法,它可以让你基于同一套源码为不同的设备终端提供最佳的展示。
熟悉响应式设计的同学都知道:
“响应式设计是通过CSS的媒体查询来实现的”
开发者借助CSS的媒体查询特性有条件地使用CSS规则,比如媒体查询根据断点(屏幕分辨率节点):
它们会告诉浏览器应该根据用户的设备忽略或应用某些规则:
最终用户在不同的终端会看到不一样的效果:
事实上,现在社区中实现响应式设计采用的大都是该方案,很多优秀的CSS框架都会有一些预定义的断点。不过基于断点来做媒体查询条件判断,特别是在一些CSS框架中预定一些断点,难免有点武断。这好比有点像猜测和妥协一样。我们越想让它工作得更好,需要设计(处理)的东西就越多。这也造就响应式设计基于媒体查询来实现总是令人感到不太优雅,效率也不高。
另外响应式设计在断点之间的切换时,流畅度是有一定的折扣(会闪那么一下),而且在响应式排版中也有着令开发者感到头痛的事情,尤其是文本的排版以及元素间距间的控制。也正因如此,才会有CSS锁的出现。
CSS锁简介
简单地说,CSS锁是一种响应式的Web设计技术,他主要致力于解决响应式设计的文本排版,它允许你根据当前的视窗大小大两个值之间平稳地转换,而不是直接从一个值跳到另一个值。
CSS锁是@Mike Riethmuller在2015年的《Precise control over responsive typography》中首次展示的,但早在2012年的时候@Tim Brown在《Flexible typography with CSS locks》中就提出了该概念,并称之为CSS Locks。
@Tim Brown用了实际生活中的一个示例来描述CSS锁。在运河和河流中都会备有船闸用来控制河中水位,也可以在不同水位的水域之间升降船只:
用到CSS锁上来的话,就是CSS锁允许你设置最小和最大的字体大小。最小字体大小将应用于最小视窗宽度以下,最大字体大小将应用于最大视窗宽度之上,在最小宽度和最大宽度之间,字体大小将按比例从最小字体到最大字体间按一定的比例缩放:
如果用CSS来描述的话会像下面这样:
/**
* 1. minf: 最小font-size
* 2. maxf: 最大font-size
* 3. minw: 视窗最小宽度
* 4. maxw: 视窗最大宽度
*/
font-size: calc([minf]px + ([maxf] - [minf]) * ( (100vw - [minw]px) / ([maxw] - [minw]) ));
@media only screen and (max-width: [minw]px) { font-size: [minf]px; };
@media only screen and (min-width: [maxw]px) { font-size: [maxf]px; };
现在我们大致知道CSS锁是什么?那么接下来,我们再花点时间来了解一下,其设计原理。
CSS锁设计原理
接下来通过一些数学计算来向大家阐述CSS锁的设计过程。
CSS中的数学计算和其他程序语言有所不同,在CSS中需要借助
calc()
函数来做数学计算,有关于该方面的介绍可以阅读《Keep Math in the CSS》和《CSS3的calc()
使用》。
视窗单位
先由视窗单位来说起。可能很多同学会和我一样,在做响应式布局时对于文本的排版,比如font-size
会采用视窗单位:
h1 {
font-size: 4vw;
}
期望的是标题在小屏幕有小的字体,大屏幕有大的字体。如果直接使用视窗单位可能会存在两个缺陷:
- 文本在小屏幕上非常小,比如在
320px
屏幕下font-size
为12.8px
;在大屏幕时非常大,比如1500px
屏幕下font-size
为60px
- 它没办法根据用户的喜好来设置文本字号大小
而CSS锁技术旨在解决第一个问题,当然该技术也在尝试着解决第二个问题:遵循用户的喜好来设置字号大小。
CSS锁的计算
前面的内容告诉我们,“CSS锁是一种特定类型的CSS值计算”。比如font-size
和line-height
。比如计算font-size
的代码:
font-size: calc([minf]px + ([maxf] - [minf]) * ( (100vw - [minw]px) / ([maxw] - [minw]) ));
上面的calc()
计算涵盖了:
minf
:最小字号maxf
:最大字号minw
:视窗最小宽度(断点1
)maxw
:视窗最大宽度(断点2
)- 在这两个断点之间的实际值是从最小值到最大值之间变化的,该变化是一个线性的变化
比如,我们在低于320px
(minw
)使用的font-size
是20px
(minf
),而高于960px
(maxw
)使用的font-size
是40px
(maxf
);在断点320px ~ 960px
之间采用的font-size
是20px ~ 40px
(minf~maxf
),用水位图来描述的话像下面这样:
如果用图表来描述的话,大致像下面这样:
用CSS可以像下面这样描述:
h1 {
font-size: 20px;
}
@media (min-width: 320px) {
h1 {
font-size: /* 从20px到40px之间 */;
}
}
@media (min-width: 960px) {
h1 { font-size: 40px; }
}
我们要接受的第一个挑战是真正的实现这个魔幻的值(自动变化的值)。在这里首先引入一个新的值,该值被称为视窗相对值,并且通过calc()
来做值的计算:
h1 {
font-size: calc(20px + '视窗相对值')
}
视窗相对值可以是一个单一的值,比如
4vw
,也可以是一个更复杂的计算值(也是基于vw
或另一个视窗单位计算得来的值)。有关于视窗单位的更多介绍可以阅读《CSS 的值和单位》一文。
由于计算是基于视窗单位的,所以CSS锁有一定的限制。它们只对数字值有效,可以使用calc()
,并且可以接像像素值。为什么是像素值呢?那是因为,浏览器客户端最终计算出来的值都是px
值。也就是说,视窗单位vw
、vh
、vmin
和vmax
总是会解析成px
。例如,如果视窗宽度是750px
,那么1vw
就相当于7.5px
。
在移动端上的一些适配方案,也就是基于这样的原理来做的。比如《再聊移动端页面的适配》中所介绍的技术方案。
从理论上来说,CSS锁对于所有应用数值的属性都应该是有效的,但事实上,它的有用性还是有限的,其中我们不能有效的查询元素,并且将每个元素的动态绑定到媒体查询上是件复杂的事情。为了让事情变得简单化,接下来我们主要聊的相关技术和特性可以运用于font-size
或line-height
(或许有一天可以运用于其他的属性,比如width
、height
等)以及它们是如何基于px
或基于em
的断点来构建CSS锁。
特别声明,下面中用到的数学公式来自于《The math of CSS Locks》一文
基于像素断点的CSS锁
先来看基于像素为单位的断点的CSS锁计算。
对于CSS锁而言,我们期望的font-size
(或line-height
)值在两个断点之间是按比例增长的。而且这种增长是一种线性增长。用图表来表示的话,像下面这样:
上图中红色线其实就是一个简单的线性函数。如果用公式来表达的话,可以是y = mx + b
,其中:
y
表示是font-size
(纵轴)x
表示的是视窗宽度,单位为像素(横轴)m
是函数的斜率(“视窗宽度每增加1px
,对应的font-size
应该增加多少像素?”)b
是在添加任何基于视窗值之前的font-size
大小
其中m
和b
在y = mx + b
中是一个不可变的部分。接下来我们要做的是算出m
和b
各自的值。先来看m
的值如何计算。
在计算m
我们需要两个点的坐标值,从图表上可以看到x
和y
在红线上都会有一个交点,其中每个点的坐标就是(x, y)
。比如视窗宽度为320px
时,x
的值为320
,对应y
的值为20
(即font-size
是20px
);同样的,视窗宽度960px
和font-size
为40px
也有一个对应的坐标,即(960, 40)
。加上m
其实表示的就是图表中红线的斜率,即可“字体大小增长值与视窗宽度增长值与的比例”。这样一来,我们就可以得到:
m = 字体大小增长值 / 视窗宽度增长值
m = (y2 - y1) / (x2 - x1)
m = (40px - 20px) / (960px - 320px) = 20 / 640 = 0.03125
还可以像下面这样简单的来表述:
font-size
增加了20px
,即在y
轴上的增长值是20
(40-20
);视窗宽度增加了640px
,即在x
轴上增长值是640
(960 - 320
)。如果视窗宽度只增加1px
,那么对应的font-size
增加就是20 / 640 = 0.03125px
。也就是m
的值为0.03125px
计算出了m
的值,那么再要计算出b
的值就不是难事了。我们可以根据y = mx + b
公式即可转换出b = y - mx
,即**b = y - 0.03125x
**。
这样我们就得到了m
和b
的值,其实我们也可以使用断点的坐标值来验证,比如断点1
的坐标(x1, y1
),即(320, 20)
,套用到公式中:
b = y1 - 0.03125 × x1 = 20 - 0.03125 × 320 = 10
或者用断点2
坐标(x2, y2
)也可以验证:
b = y2 - 0.03125 x x2 = 40 - 0.03125 x 960 = 10
这样一来,将m
和b
套到公式y = mx + b
中的话,线性函数就变成了y = 0.03125x + 10
。公式中的y
对应的是font-size
,那么在CSS中的话,我们就可以通过calc()
像下面这样表达出来:
font-size: calc( 0.03125 * x + 10px)
如果就像上面那样使用的话,将是一行无效的CSS代码,因为x
不是有效的CSS语法。但在公式中x
对应的视窗的宽度,在CSS中可以用100vw
来表示,线性函数在CSS中可以像下面这样有效的表达出来:
font-size: calc( 0.03125 × 100vw + 10px)
根据calc()
的语法规则,上面的公式可以变得更短一些:
font-size: calc(3.125vw + 10px)
而这个值对于我们来说是期望在两个断点之间的font-size
的值,那么使用CSS媒体查询就可以很好的表述出来:
h1 { font-size: 20px; }
@media (min-width: 320px) {
h1 {
font-size: calc( 3.125vw + 10px );
}
}
@media (min-width: 960px) {
h1 {
font-size: 40px;
}
}
如果用图表来描述的话就会像下图这样:
不过很多同学可能不太喜欢给font-size
使用固定单位(比如px
),而是更喜欢采用一些相对单位(比如rem
、em
和%
等)。接下来,我们将上面公式中的px
单位换成rem
单位。
注意,
em
和%
单位的使用和rem
类似
要在公式中使用rem
这样的单位值,首先要确保的是根字体大小没有被固定值覆盖,即root
(也就是html
)的font-size
不是固定值。我们可以像下面这样设置html
的font-size
值:
html {
font-size: 62.5%;
}
在用户未显式设置客户端字体大小的话,那么浏览器默认的font-size
是16px
,这样一来:
html {
font-size: 62.5%; /* 该值相当于.625 × 16px = 10px */
}
如果换成rem
的话,62.5%
相当于 1rem
,即62.5% ▶ 1rem = 10px,.1rem = 1px
。如果将html
的font-size
设置为125%
的话(即16 × 1.25 = 20px
),即125% ▶ 1rem = 20px,.05rem = 1px
。
也就是说,将html
的font-size
大小保持不变,即始终为16px
。这样的话:
10px ▶ .625rem
,20px ▶ 1.25rem
,40px ▶ 2.5rem
将上面的值换到CSS锁中:
h1 { font-size: 1.25rem; }
@media (min-width: 320px) {
h1 {
font-size: calc( 3.125vw + .625rem );
}
}
@media (min-width: 960px) {
h1 {
font-size: 2.5rem;
}
}
如果我们使用浏览器默认的字体大小,即16px
,上面这段代码和前面基于px
代码起到的效果是相同的。而我们的目标是能让用户根据自己的喜欢去设置,也就是说,如果用户将浏览器的默认的font-size
改成自己所需要的字号,比如说24px
(在16px
的基础上增加了50%
),这个时候上面的CSS锁运行之后,效果可能会如下图这样所示:
上图中蓝色点色表示浏览器默认字号是16px
,未连接在一起红色表示浏览器默认字号是24px
。
从图中可以看出来,在断点1
(320px
)处,font-size
大小实际上变小了(从30px
跳到了25px
),而在断点2
(960px
)处,font-size
大小反而变大了(从45px
跳到了60px
)。整个红线是断层的,不像蓝色点线那样能连接在一起。这样和CSS锁的初衷是不同的。
要解决这个问题,我们可以对所有三种大小使用相同的用户可配置基线。例如,我们选择1.25rem
作为用户配置的基线值,这样一来,上面的CSS锁就变成:
h1 { font-size: 1.25rem; }
@media (min-width: 320px) {
h1 {
font-size: calc( 1.25rem + 3.125vw - 10px );
}
}
@media (min-width: 960px) {
h1 { font-size: calc( 1.25rem + 20px ); }
}
看到代码中的3.125vw - 10px
?其实就是前面所说的线性函数(mx + b
)相似,只不过这里的b
的值有所不同,我们暂且将它称为**b′
**,因为我们的基线值等于20px
,我们可以像下面这样来计算b′
的值:
b′ = b - 用户可配置基线值
b′ = 10px - 20px
b′ = -10px
另一种策略就是一开始选择好基线值,然后再寻找描述font-size
增加的线性函数,这里为了和前面的线性函数区分出来,暂且命名为y'
(也可以用来区别完整的字体大小y
)。
// x轴,表示视窗宽度,x1和x2分别是断点1和断点2视窗的宽度
x1 = 320
x2 = 960
// y轴,表示font-size大小
y'1 = 0
y'2 = 20
// 斜率
m = (y'2 - y'1) / (x2 - x1)
m = (20 - 0) / (960 - 320) = 20 / 640 = 0.03125
b' = y' - mx
b' = y'1 - 0.03125 × x1 = 0 - 0.03125 × 320 = -10
这样一来,我们得到了y' = 0.03125x - 10
(前面的线性函数是y = 0.03125x + 10
),如果用图表来描述的话:
这样我们基本值是rem
或者是vw
还是px
都可以得到完全工作CSS锁(在font-size
上的CSS锁)。当用户改变他们的基本字体大小时,整个东西就会上升或下降,而不再会中断。如下图所示:
上面我们看到的是CSS锁在font-size
上的运用,接下来,我们来看看CSS锁在line-height
上的运用。
和font-size
类似,假设我们在断点1
(视窗宽度为320px
)处设置的line-height
是1.4
,而在断点2
(视窗宽度为960px
)处设置的line-height
是1.8
。
在《编写CSS时要考虑可访问性》一文中我们可以获知,
line-height
设置的好坏,直接会影响可读性,设置恰当的line-height
在视觉上也更能吸引人。
CSS中的line-height
在排版中的运用相对来说是非常复杂的,比如说,不同的单位计算,结果也有相应的差异,如下图所示:
从上图中可以看出,设置line-height
的最佳实践是 不带任何单位设置行高的值。有关于这方面的知识,这里不作过多的阐述,感兴趣的可以阅读:
接下来,我们回到我们要聊的CSS锁中。通过前面的学习,我们知道,在CSS锁中会使用一个基础值加上一个以像素表示的动态值,那么在line-height
设置中,我们首先要知道是 1.4
和1.8
的比率涉及多少像素。也意味着我们需要知道段落(文本)的font-size
。假设我们的段落文本使用默认的font-size
,即16px
,那么line-height
的值相应为:
// 断点1,视窗宽度为320px处的line-height
16 × 1.4 = 22.4px
// 断点2,视窗宽度为960px处的line-height
16 × 1.8 = 28.8px
假设我们使用1.4em = 22.4px
作为基线,那么6.4px
(28.8px - 22.4px
)就是增量。这样一来,对应的线性函数:
x1 = 320
x2 = 960
y′1 = 0
y′2 = 6.4
m = (y′2 - y′1) / (x2 - x1)
m = (6.4 - 0) / (960 - 320)
m = 6.4 / 640
m = 0.01
b′ = y′ - mx
b′ = y′1 - 0.01 × x1
b′ = 0 - 0.01 × 320
b′ = 3.2
y′ = 0.01x - 3.2
用CSS来描述的话是:
line-height: calc( 1.4em + 1vw - 3.2px)
特别声明,这里基值使用
1.4em
主要是为了处理各浏览器下calc()
的兼容性,因为使用不带单位的1.4
,在所有浏览器中的calc()
都不支持,如果使用140%
,虽然在Chrome和Firefox中得到支持,但在Safari中不起作用。
那么相应的line-height
对应的CSS锁就如下所示:
p { line-height: 1.4em; }
@media (min-width: 320px) {
p {
line-height: calc( 1.4em + 1vw - 3.2px );
}
}
@media (min-width: 960px) {
p {
line-height: calc( 1.4em + 6.4px );
}
}
另外,对于大的值,我们不能只使用1.8em
,因为我们需要添加到基础上的部分是用px
表示的。也就是说,要是我们直接使用1.8em
的话,对于16px
的基本字号来说,结果还是不错的,但用户更改了字号的基本值时,结果就有可有不一样了。
我们可以用函数图像来检查它是否适用于不同的基本字号大小:
也就是说,由于line-height
计算公式取决于元素本身font-size
的大小,如果我们更改font-size
大小,就必须更改相应的公式。例如下面这个示例font-size
是1.66em
,不再是16px
,那么对应的line-height
计算就是:
// 元素的line-height
.big {
font-size: 1.66em
}
// 断点1,视窗宽度为320px处的line-height
16 × 1.66 × 1.4 = 37.184px
// 断点2,视窗宽度为960px处的line-height
16 × 1.66 × 1.8 = 47.808px
根据前面的运算过程,我们可以得到最终的线性函数公式为y' = 0.0166x - 5.312
。这样就可以得到最终的CSS锁:
p {
line-height: 1.4em;
}
.big {
font-size: 1.66em;
}
@media (min-width: 320px) {
p {
line-height: calc( 1.4em + 1vw - 3.2px );
}
.big {
line-height: calc( 1.4em + 1.66vw - 5.312px );
}
}
@media (min-width: 960px) {
p {
line-height: calc( 1.4em + 6.4px );
}
.big {
line-height: calc( 1.4em + 10.624px );
}
}
另一个选项是让CSS进行计算。因为在标准段落使用相同断点和相对line-height
,所以我们只需要添加一个1.66
因子:
p {
line-height: 1.4em;
}
.big {
font-size: 1.66em;
}
@media (min-width: 320px) {
p {
line-height: calc( 1.4em + 1vw - 3.2px );
}
.big {
line-height: calc( 1.4em + (1vw - 3.2px) * 1.66 );
}
}
@media (min-width: 960px) {
p {
line-height: calc( 1.4em + 6.4px );
}
.big {
line-height: calc( 1.4em + 6.4px * 1.66 );
}
}
上面看到的分别是font-size
和line-heihgt
在以px
为单位断点下的CSS锁。其实我们还可以将他们结合在一起。假设我们的排版中有标题h1
和段落p
,他们对应的font-size
和line-height
分别如下表所示:
元素 | 属性 | 断点1 (320px ) |
断点2 (960px ) |
---|---|---|---|
h1 |
font-size |
24px |
40px |
h1 |
line-height |
1.33em |
1.2em |
p |
font-size |
15px |
18px |
p |
line-height |
1.5em |
1.66em |
按排版本原则来说:“文本变大时(font-size
更大),行高(line-height
)要更紧凑一点;当宽度变宽时,行高(line-height
)要更松散一点”。这样一来,在我们这个场景中,两个原则都使用的话就会相互矛盾。所以我们只会考虑其中一方面:
- 对于标题
h1
来说,字体大小(font-size
)的增加将比宽度增加更显著 - 对于段落
p
来说,宽度的增加比字体大小(font-size
)只增加一点,效果会更显著
现在,我们继续基于320px
和960px
两个断点,先来完善font-size
的CSS锁:
h1 {
font-size: 1.5rem; /* 24px / 16px 浏览器默认font-size为16px */
}
/* 0.9375rem = 15px */
p {
font-size: .9375rem; /* 15px / 16px*/
}
@media (min-width: 320px) {
h1 {
font-size: calc( 1.5rem + 2.5vw - 8px)
}
/* .46875vw - 1.5px的结果是0 ~ 3px*/
p {
font-size: calc(.9375rem + .46875vw - 1.5px)
}
}
@media (min-width: 960px) {
h1 {
font-size: calc(1.5rem + 16px)
}
p {
font-size: calc(.9375rem + 3px)
}
}
接下来再来完成line-height
的CSS锁,相对来说要比font-size
的CSS锁更复杂一点。
先从h1
开始。我们想给line-height
设置一个相对基线的值,所以最低的值1.2em
(上面表格所列的最小值),加上元素的字体大小是可变的,所以1.2em
将描述一个动态的线性值,该线性值具备下面两个特征:
24 × 1.2 = 28.8px
在断点1
(320px
)处的值40 × 1.2 = 48px
在断点2
(960px
)处的值
从表格中我们也知道,在断点1
处,期望的line-height
的值是1.33em
,因此我们可以将该值四舍五入为32px
。另外我们希望找到一个线性函数可以来描述“我们添加到1.2em
基线的内容”。如果我们从数据点中删除这个1.2em
基线值,就会重新得到两数据:
24 × (1.3333 - 1.2) = 3.2px
在断点1
(320px
)处的值40 × (1.2 - 1.2) = 0px
在断点2
(960px
)处的值
放到我们前面提到的公式中来计算:
m = (y′2 - y′1) / (x2 - x1)
m = (0 - 3.2) / (960 - 320)
m = -3.2 / 640
m = -0.005
b′ = y′ - mx
b′ = y′1 - (-0.005 × x1)
b′ = 3.2 + 0.005 × 320
b′ = 4.8
y′ = -0.005x + 4.8
最终结果用CSS来描述的话,像下面这样:
h1 {
line-height: calc( 1.2em - .5vw + 4.8px)
}
同样可以用图表来描述行高和字体大小函数之间的关系:
对于段落p
,将使用1.5em
作为基线。我们要增加的行高是:(1.75 - 1.5) × 18 = 4.5px
。
虽然上面的计算都是人肉的在计算,但效果确实是存在的。为了减少人肉计算过程造成的错误,我们可以选择整个过程通过CSS的计算来完成。比如下面这个示例:
@media (min-width: 320px) and (max-width: 959px) {
h1 {
font-size: calc(
/* y1 */
1.5rem
/* + m × x */
+ ((40 - 24) / (960 - 320)) * 100vw
/* - m × x1 */
- ((40 - 24) / (960 - 320)) * 320px
);
}
}
可以将上面的代码进一步简化:
@media (min-width: 320px) and (max-width: 959px) {
h1 {
font-size: calc( 1.5rem + 16 * (100vw - 320px) / (960 - 320) );
}
}
如果把行高的锁一起结合进来像下面这样:
@media (min-width: 320px) and (max-width: 959px) {
h1 {
font-size: calc( 1.5rem + 16 * (100vw - 320px) / (960 - 320) );
/* 对于负的斜率,需要对断点求倒数 */
line-height: calc( 1.2em + 3.2 * (100vw - 960px) / (320 - 960) );
}
}
基于em
断点的CSS锁
首先要说的是在CSS媒体查询中使用em
做为断点时,CSS锁的计算会存在很多陷阱。比如说,我们前面的示例中使用m × 100vw
(在CSS中类似calc( base + 2.5vw)
)就会有问题存在。主要因为在媒体查询的上下文中,em
或rem
单位都是一种相对单位,其最终大小都会取决于别人。比如rem
就是基于用户代理的基本字体大小,虽然默认情况下是16px
,但它可有可能会更小或更大,这主要取决于两个因素:
- 浏览器或操作系统选项
- 用户的偏好
这也意味着,如果我们的断点是20em
和60em
,它可能匹配的实际宽度是:
- 基于
font-size
为16px
的场景,20em
和60em
所匹配的是320px
和960px
- 基于
font-size
为24px
的场景,20em
和60em
所匹配的是480px
和1440px
- 如果浏览器默认字体大小更换,那么对应的值也会再次更改
比如我们前面示例中的:
font-size: calc( 3.125vw + .625rem );
如果断点换成em
来计算,假设媒体查询中的1em
为16px
,那么CSS锁对应媒体查询中的px
换成了em
,代码像下面这样:
h1 {
font-size: 1.25rem;
}
@media (min-width: 20em) {
h1 {
font-size: calc( 1.25rem + 3.125vw - 10px );
}
}
@media (min-width: 60em) {
h1 {
font-size: calc( 1.25rem + 20px );
}
}
就上面的代码而言,如果客户端浏览器的默认字体大小为16px
的话,上面的CSS锁运行不会有任何缺陷,但是用户将客户端浏览器默认字号更换了,那么就会造成相应的混乱。如果用图表来表示的话,正常的是连续性的,不正常的就会断连:
造成这样的现象主要是我们更改了基本字体大小。因为基于em
的断点将移动到更大的像素值。在公式中的3.125vw - 10px
值仅适用于特定的像素点(基于像素的媒体查询)。如果更换了场景,那么结果就会差强人意:
- 在
320px
时,3.125vw - 10px
的结果是0px
,这是我们计划中也是我们想要的结果 - 在
480px
时,3.125vw - 10px
的结果是5px
- 在
960px
时,3.125vw - 10px
的结果是20px
- 在
1440px
时,3.125vw - 10px
的结果是35px
如果拿320px
和480px
两个断点来比较的话,3.125vw - 10px
在两个断点的差值是5px
,还能让人接受。但在960px
和1440px
两个断点间,3.125vw - 10px
的差值为15px
,该差值过于偏大。
基于上述原因,我们如果想在媒体查询中使用em
作为断点的话,就需要对CSS锁进行改造,换句话说需要一种不同的技术来实现CSS锁。
插句题外话,在媒体查询中使用px
、em
还是rem
单位,哪个更适合,在社区中也有很多相关的讨论,如果你对这方面的话题感兴趣的话,可以阅读下面这些文章:
- PX, EM or REM Media Queries?
- Don't Use Em for Media Queries
- EM vs REM vs PX – Why you shouldn't “just use pixels”
- Using Media Queries For Responsive Design In 2018
- The EMs have it: Proportional Media Queries FTW!
- To em or not to em? That is the media query question.
- Pixels vs. Relative Units in CSS: why it’s still a big deal
我们回到以em
为单位的媒体查询的CSS锁计算中来。在以em
为断点的媒体查询计算CSS锁,我们需要依赖CSS做大量的计算,在计算中会用到两个变量:
- 视窗宽度为
100vw
- 在低的断点下,采用
rem
为单位
将采用的计算公式为:
y = m × (x - x1) / (x2 - x1)
该公式是如何得来的呢?我们简单地来看看。在前面的内容中,我们知道字体大小或行高可以用一个线性函数来描述,比如y = mx + b
。在CSS中,虽然可以使用100vw
来替代公式中的x
,但无法将m
和b
用精确的px
或vw
值来描述,其中原因是我们采用的是以px
为单位的固定值。如果更改基本字体大小,它们将与基于em
的断点会不相匹配。如此一来,就需要考虑用别的方式来替代m
和b
,也就是两个数据点(x1, y1)
和(x2,y2)
。在前面的内容中,我们知道如何从线性函数中得到b
:
b = y - mx
b = y1 - m × x1
将它们放在一起的话,可以得到:
y = mx + b
y = mx + y1 - m × x1
最后的结果,我们可以看到,在整个函数中已经没有b
的身影了。同样,将font-size
和line-height
的基线值添加进来,从前面的知识中我们可以知道,该值是一个动态值,我们把这个动态部分用``表示,那么整个公式可以是:
y = y1 + y′
y′ = y - y1
上面的方式式可以继续演进成下面这样的:
y′ = mx + y1 - m × x1 - y1
y′ = mx + y1 - m × x1 - y1
可以进一步优化:
y′ = m × x - m × x1
y′ = m × (x - x1)
根据上面的公式,我们就可以轻意得到m
的值:
m = (y2 - y1) / (x2 - x1)
也就是:
y′ = (y2 - y1) / (x2 - x1) × (x - x1)
上面的公式还可以换成:
y′ = 最大值的增长 × (x - x1) / (x2 - x1)
接下来将上面的公式用CSS代码来描述,我们接着回到20px ~ 40px
的示例中:
@media (min-width: 20em) and (max-width: 60em) {
h1 {
/* 警告: 这还不行! */
font-size: calc(
1.25rem /* 基线值 */
+ 20px /* 最大值与基线值的差值 */
* (100vw - 20rem) /* x - x1 */
/ (60rem - 20rem) /* x2 - x1 */
);
}
}
注意,上在代码中的
calc()
计算中使用相同或不同的值进行行乘法和除法时有许多限制,因此上面的代码在运行时可能会有问题
先回到100vw - 20rem
的计算,该计算可以正常,并将返回一个像素值。为什么呢?我们来看一个小示例。如果基本字体的大小是16px
,那么视窗的宽度就是600px
,结果将是280px
(600 - 20 × 16 = 280
);如果基础字体大小是24px
,视窗宽度
为600px
,则结果为120px
(600 - 20 × 24 = 120
)。
可能你会问,既然em
存在问题,我们为何不考虑采用rem
呢?因为rem
毕竟在计算的过程是基于根元素html
或:root
来做计算的。这个原理是对的,但我们需要一个CSS单位来引用浏览器的基本字体大小,但这个单位并不存在。在CSS中最接近的只能是在html
元素或:root
中来做,如下面的代码:
/* 不好的 */
html { font-size: 10px; }
:root { font-size: 16px; }
/* 但在媒体查询的断点我们需要这样来进行管理,比如20rem / 1.25 或 40em / 1.25这样 */
:root { font-size: 125%; }
这样一来,把事情变得更为复杂化。为了让计算变得更简单,接下来在计算中引入无单位的因子计算。先来看理想情况之下的现象。
在理想的情况之下,我们期望60rem ~ 20rem
部分解析为像素宽度。这意味着整个(x - x1) / (x2 - x1)
结果会分解为0 ~ 1
之间的值,在这里,将该值暂且称为 n
。
那么在基本字体大小为16px
,视窗宽度为600px
的情况下,我们会得到:
n = (x - x1) / (x2 - x1)
n = (600 - 320) / (960 - 320)
n = 280 / 640
n = 0.475
遗憾的是,上面的公式如果要用CSS的calc()
来转换的话,有可能是不能工作的。主要是因为在calc()
的除法计算中除数不能带有任何单位。不过我们可以尝试着将除数中单位去除,比如calc( (100vw - 20rem) / (60 - 20) )
的结果:
视窗宽度(断点) | calc() 计算 |
结果 |
---|---|---|
20em (320px ) |
calc((320px - 16px * 20) / (60 - 20)) |
0px |
40em (640px ) |
calc((640px - 16px * 20) / (60 - 20)) |
8px |
60em (960px ) |
calc((960px - 16px * 20) / (60 - 20)) |
16px |
上面表格的计算是基于基本字体大小为16px
(客户端浏览器默认字体大小为16px
)。
视窗宽度(断点) | calc() 计算 |
结果 |
---|---|---|
20em (480px ) |
calc((480px - 24px * 20) / (60 - 20)) |
0px |
40em (960px ) |
calc((960px - 24px * 20) / (60 - 20)) |
12px |
60em (1440px ) |
calc((1440px - 24px * 20) / (60 - 20)) |
24px |
上面表格的计算是基于基本字体大小为24px
(客户端浏览器默认字体大小为24px
)。
你可能也注意到了,断点20em ~ 60em
之间对应得到的线性变化的值是0 ~ 1em
之间。该特性我们可以用起来。接下来在CSS的calc()
中计算时采用20
作为因子数:
font-size: calc( 1.25rem + 20px * n );
上面公式中的n
是一个0 ~ 1
之间的任意值,但在CSS的calc()
中,并无法得到0 ~ 1
之间对应的结果。为此,将该值暂时用r
来描述。
这里需要注意,r
是一个带单位的数值,但在calc()
的乘法计算中,其中一个数是不能带任何单位的。也就是说,和r
相乘的另一个数是需要不带单位的。基于这个原因,在我们的示例中,我们希望在较大的断点处增加20px
(20px
对应的是1.25rem
),所以
示例中采用的系数是1.25
:
font-size: calc( 1.25rem + 1.25 * r );
上面的代码在CSS中是可以工作了,但要注意r
是根据基本字体大小而变化:
- 基本字体大小是
16px
,那么1.25 * r
的值在0 ~ 20px
之间 - 基本字体大小是
24px
,那么1.25 * r
的值在0 ~ 30px
之间
这样一来,我们的CSS锁则变成像下面这样:
h1 {
font-size: 1.25rem;
}
@media (min-width: 20em) {
/* (100vw - 20rem) / (60 - 20) 计算出断点 (20em ~ 60em)之间 0 ~ 1rem 对应的值 */
h1 {
font-size: calc( 1.25rem + 1.25 * (100vw - 20rem) / (60 - 20) );
}
}
@media (min-width: 60em) {
h1 {
font-size: calc( 1.25rem + 1.25 * 1rem );
}
}
这与基于px
的font-size
锁不同,这一次,当用户将基本字体大小增加50%
时,所有内容都会增加50%
(基线值,可变部分和断点)。我们得到了一个30px ~ 60px
的范围,而不是默认的20px ~ 40px
的范围。
按同样的原理,我们可以得到line-height
的CSS锁:
h1 {
line-height: calc( 1.2em + 0.2 * 1rem );
}
@media (min-width: 20em) {
h1 {
line-height: calc( 1.2em + 0.2 * (100vw - 60rem) / (20 - 60) );
}
}
@media (min-width: 60em) {
h1 {
line-height: 1.2em;
}
}
有关于
line-height
的CSS锁更多的介绍还可以阅读《Molten leading (or, fluid line-height)》一文。
将font-size
和line-height
结合在一起,CSS锁对应的代码如下:
@media (min-width: 320px) and (max-width: 959px) {
h1 {
font-size: calc( 1.5rem + 16 * (100vw - 320px) / (960 - 320) );
/* For a negative slope, we have to invert the breakpoints */
line-height: calc( 120% + 3.2 * (100vw - 960px) / (320 - 960) );
}
}
重构CSS 锁
@trysmudford 对传统的CSS锁做了一些重构,在重构的CSS锁中引入了CSS自定义属性,让整个CSS锁变得更灵活。有关于整个重构的过程可以阅读《Refactoring CSS Locks》一文。最终重构出来的代码如下:
:root {
--fluid-min-screen: 20;
--fluid-max-screen: 80;
--fluid-viewport: 100vw;
--fluid-bp: (
(var(--fluid-viewport) - calc(var(--fluid-min-screen) * 1em)) / (var(
--fluid-max-screen
) - var(--fluid-min-screen))
);
}
h2 {
font-size: calc(2em + (4 - 2) * var(--fluid-bp));
}
h3 {
font-size: calc(1.5em + (3 - 1.5) * var(--fluid-bp));
}
h4 {
margin-top: calc(0.5em + (2 - 0.5) * var(--fluid-bp));
}
hr {
border: calc(0.1em + (4 - 0.1) * var(--fluid-bp)) solid teal;
}
@media screen and (min-width: 80em) {
:root {
--fluid-viewport: calc(var(--fluid-max-screen) * 1em);
}
}
如果你不想人肉的去处理CSS锁的话,我们可以在工程中使用**postcss-scale**,可以让事情变得更简单:
@media screen and (min-width: 21em) {
p {
line-height: scale(21em, 35em, 1.3em, 1.5em, 100vw);
}
}
编译出来:
@media screen and (min-width: 21em) {
p {
line-height: calc(((1.5 - 1.3) * ((100vw) - 21em) / (35 - 21)) + 1.3em);
}
}
RFS
RFS是Responsive Font Size的缩写,该技术也是基于CSS锁上面做的。比如说:
html {
font-size: 16px;
}
@media screen and (min-width: 320px) {
html {
font-size: calc(16px + 6 * ((100vw - 320px) / 680));
}
}
@media screen and (min-width: 1200px) {
html {
font-size: 22px;
}
}
在Sass中我们可以像下面这样使用RFS:
.title {
@include font-size(4rem);
}
编译出来的结果:
.title {
font-size: 4rem;
}
@media (max-width: 1200px) {
.title {
font-size: calc(1.525rem + 3.3vw);
}
}
下面这张图表可以帮助我们更好的理解RFS是如何调整字体大小的:
有关于RFS更多的介绍可以阅读:
- RFS
- Using a PostCSS function to automate your responsive workflow
- Using a Mixin to Take the Math out of Responsive Font Sizes
参考资料
- Molten leading (or, fluid line-height)
- Precise control over responsive typography
- The math of CSS Locks
- Break(point) free: No more stepwise Styling in CSS
- Responsive Sizing with two-dimensional CSS Locks
- designing with fluid type scales
- css-only fluid modular type scales
- Refactoring CSS Locks
小结
自响应式Web设计的出现,对于响应式中的文本排版(后期也被称为流式文本排版或精准排版)都面临着很大的困难。也正因为如此,社区中有很多大神,比如@Mike Riethmuller、@Tim Brown和@Geoff Graham都在致力于这方面的研究,而且找到了使用CSS锁技术方案来锁定流式排版。在该文中我们将CSS锁相关的知识结合在一起,介绍了什么是CSS锁,以及其设计原理。最后希望这篇文章对于你在响应式或流式排版本中有所帮助。如果你在这方面有更好的经验或建议,欢迎在下面的评论中与我们一起共享。