“我应该使用 px 还是 em/rem?”
前言
这是一个情绪化的问题。在这句话的背后往往带有一丝焦虑或沮丧亦或是迷茫。😅
这个问题很容易引起争议,并且总是一边倒的。多数人都会认为 em/rem 会更易于访问。但这不是非此即彼的问题。在有些情况下,rem 更易于访问,而在其他情况下,px 更易于访问,又在部分情况下,需要把 px 和 rem 一起使用才能构建出最容易访问的产品。
那么在这篇文章里将会做这些事情:
- 简单介绍每个单位的工作原理,确保我们都简历在同一个扎实的基础上
- 了解可访问行的注意事项是什么,以及每个单位如何影响这些注意事项
- 建立一个心智模型,利用它来帮助我们决定在任何场景初衷使用哪个单位
- 单位之间的转换的技巧和窍门
单位概述
px
px 是在 CSS 中最常见的单位,每个人在学习之处最先接触到的单位就是它,它的用法是:
.wrapper {
width: 100px;
margin-top: 10px;
padding: 10px;
}
它的值是绝对的。这以为着 1px 就表示了电脑显示器或者手机屏幕上的单个像素点,它是最简单的一个单位,也是最不抽象的单位,最接近于 “金属”,所以它带来的感觉非常直观。
硬
硬件与软件像素
其实严格来讲 px 这个单位并不准确,因为它并没有 1:1 地映射到硬件像素上。如果你在显微镜下观察现代显示器,您会发现它们不再由清晰的小 R/G/B 矩形组成。以下是 Apple Watch 和 Apple iPhone 屏幕的特写镜头:
甚至在硬件制造商在对像素网格的进行创意排列之前,屏幕中的物理像素和我们用 CSS 编写的软件像素仍然存在区别。每次用户更改屏幕分辨率或方法时,他们都会改变软件像素映射到硬件像素的方式,但这只是其中之一。但是这些都不影响我们对 px 的感觉,它仍然是最具体、最绝对的单位。
em
em 比较有趣,它是一个相对单位,基于元素的字体大小来计算,但有一部分人会搞混的是,这个基准元素是元素本身而不是元素的父级,你可以用以下代码来验证刚刚说的:
<html>
<head>
<style>
body {
font-size: 100px;
}
div {
width: 1em;
height: 1em;
font-size: 20px;
}
</style>
</head>
<body>
<div></div>
</body>
</html>
事实上 em 是一个百分比值,如果我们将单位设置成 1.5 的话,我们就可以说它是字体大小的 1.5 倍,这其实就是一种缩放。
<style>
p {
/* Change me! */
font-size: 24px;
}
</style>
<p>
<span style="font-size: 1em">
This
</span>
<span style="font-size: 0.8em">
sentence
</span>
<span style="font-size: 0.64em">
gets
</span>
<span style="font-size: 0.5em">
quieter
</span>
<span style="font-size: 0.4em">
and
</span>
<span style="font-size: 0.32em">
quieter
</span>
</p>
rem
rem 的工作原理与 em 一致,但不同的是它的基准元素不同,em 是基于自身,而 rem 则是基于 html 元素。那么就有一个问题,为什么需要 rem 这个单位?
因为 em 这个单位是一个“轻浮”(假的)的单位,它太容易被影响了!查看一下代码:
<style>
main {
font-size: 1.125em;
}
article {
font-size: 0.9em;
}
p.intro {
font-size: 1.25em;
}
</style>
<main>
<article>
<p class="intro">
What size is this text?
</p>
</article>
</main>
.intro 这个元素的字体到底多大?为了能搞清楚这个问题,我们就必须将其的字体大小与其所有的父级相乘。默认情况下,根元素的字体大小是 16px,所以它的计算过程就是:16 * 1.125 * 0.9 * 1.25,结果是 20.25px 。
**嗯?为什么??**这是因为字体大小是可继承的。该段落的字体大小为1.25em,表示“当前字体大小的 1.25 倍”。但是当前的字体大小是多少?好吧,它是从父级继承的:0.9em. 所以它是父节点的 1.25 倍,是父节点的 0.9 倍,也是父节点的1.125倍。
事实上,我们需要将em树中的每个值相乘,直到达到“固定”值(使用像素),或者一直到树的顶部。为了解决这个问题,CSS 语言设计者创建了这个rem单元,它代表“root em”。
rem单位就像单位一样,只是它始终是根节点(元素em)上字体大小的倍数。它忽略任何继承的字体大小,并始终基于顶级节点进行计算。<html>文档的默认字体大小为 16 像素,1rem 意味着px 值为 16 像素。1rem我们可以通过更改font-size根节点上的来重新定义的值:
html {
font-size: 28px;
}
h1 {
font-size: 2rem;
}
你可以这么做,但不推荐这么做。 为了理解原因,需要谈谈 “可访问性”。
可访问性
当涉及到 px/em/rem 的时候,主要需要考虑到可访问性的是视觉。我们希望视力有限的人能够舒适地阅读我们网站和网络应用程序上的句子和段落。
视力有限的人可以通过集中方法来增加文本的大小。
- 使用浏览器的缩放功能。比如
Ctrl+Mouse Scroll或Ctrl++Web 内容可访问性指南(WCAG)中指出,为了便于访问,网站应该在 **缩放为 200% 的时候仍然保证内容与功能的可用。**这个数字对于示例正常的人来说很大,但从无障碍倡导者那里听说,这个识字确实是一个最小值,而且许多示例障碍的人的设置经常比这个数字高得多。 - 在浏览器设置中增加默认字体大小。
字体缩放通过重新定义字体大小的 “默认值” 来工作,所有相关单位将基于的默认的字体大小(rem、em、%)。之前说的 1rem 等于 16px 是在保证用户没有主动修改默认字体大小的时候,这个结论才成立!但如果他们将默认值字体大小设置为 32px,那么 1rem 则会变成 31px 而不再是 16px 了。本质上,我们可以将字体缩放这一行为看做为修改了 root 元素的字体大小。
这就是为什么我们应该使用像rem和这样的相对单位em来表示文本大小。它使用户能够重新定义他们的价值,以满足他们的需求。现在浏览器再次缩放时,页面就不会再那么糟糕了。
当用户方法或缩小时,一切都会变化,基本上它会将倍数应用于每个单元,包括像素。它影响除了视口单位 (vw/vh等)之外的所有内容。
但是,如果用户总是可以所当来增加他们的字体大小的话,我们真的需要担心支持字体缩放吗?现在的选择还不够好吗?不够!为什么?问题是缩放实际上是为了在每个站点的基础上使用的,每次访问新的站点时,有些人不得不再次调整页面的缩放比例来保证正常的浏览。但如果他们可以设置一个 “默认” 的字体大小,一个大到足以支持让用户舒适阅读的字体大小,并让这个大小得到普遍的尊重的话,那不是更好吗?(当然我们还得知道,不是每个人都可以很轻松得触发键盘快捷键)通常情况下,我们应该给用户尽可能多的控制权,并且我们永远都不应该禁用或阻止他们进行设置。处于这个原因,使用像 rem 这样的相对单位来排版就显得非常重要。
如何选择
在了解完 rem 的好处以后,你可能会想,既然 rem 这么好用,那我为什么还要使用 px?考虑以下两段代码:
.wrapper {
font-size: 1rem;
padding: 2rem;
}
.wrapper {
font-size: 1rem;
padding: 32px;
}
当 font-size 逐渐增加的情况下,两段样式带给用户的体验是一样的吗?
很明显是不一样的,当 rem 的基准值不变时,下面的体验会比上面的好得多。为什么?
请记住,rem 的值是随着默认字体大小缩放的,当涉及到页面中布局排版的时候,这不是一件好事。这中间存在着对文本大小方面的暗坑。文本越大,每行可以容纳的字符就越少,当用户将文本提高至 250%的时候,每行能展示的文本可能就几个单词或是几个文字。
当我们在使用 rem 来进行填充时,我们放大了这种负面影响,我们在做一件会导致页面减少可用空间的数量的事,这会进一步限制每行能容纳的文本内容。这种体验非常糟糕,因为每行的内容太有限了,阅读起来会感到非常累。同样的,如果将边框宽度也设置为 rem 的时候,这也是没必要的,因为用户在方法页面的时候,边框宽度变得更粗这对阅读来讲没有任何意义,反而会导致空间进一步被挤压,不是吗?
所以这就是为什么我们要战略性地使用这些单位的原因,在 px 和 rem 之间选择的时候,我们需要问自己:“这个值是否应该随着用户增加浏览器的默认字体大小而进行缩放?”
这个问题便是这个心智模型的关键,如果这个值需要随着默认字体大小进行缩放,那应该使用 rem,否则就应该使用 px,比如边框宽度。但这个问题并不总是那么容易得出答案,我们看一些例子:
媒体查询
在使用媒体查询的时候,应该使用 rem 还是 px?
@media (max-width: 800px) {
// ...
}
@media (max-width: 50rem) {
// ...
}
只是这么对比答案还是不太明显,我们尝试分解一下。
假设用户设置默认字体大小为 32px,是标准文本大小的两倍,这以为着 50rem 现在是 1600px,而不是 800px。我们先别太过于在意这个值是多少,试着将它当做是 800px 来看待。如果你是一个视力障碍者,但你使用的是 1600px 的设备来访问网站,那么你可能会看到手机端的布局,直到设备的分辨率大于这个值才可能看到桌面端布局。
乍一看好像这个逻辑并没有问题,你并不是移动端用户,为什么网页需要给你展示移动端的布局呢?然而通常情况下,我们通常是希望使用 rem 来进行媒体查询。
然而,当我们使用基于 rem 的媒体查询时,我们会退回到“移动端”布局。结果,内容变得更具可读性,并且体验得到了很大改善。我们习惯于从移动/平板电脑/台式机的角度考虑媒体查询,但我认为从**可用空间的角度考虑更有帮助。**移动用户的可用空间比桌面用户少,因此我们设计了针对该空间量进行优化的布局。类似地,当有人调大他们的默认字体大小时,他们会*减少可用空间量,*因此他们可能应该得到相同的优化。
外边距
另一个场景是,我们使用 rem 来设置元素的外边距。当我们的文本大小越来越大的时候,我们就应该**使用 rem 来进行边距设置。**这又是为什么?
这是因为随着字体大小的变化,如果仍然使用 px 来进行设置,并且边距设置得不够大,而字体越来越大的时候,从视觉观感上来讲,我们得到的反馈就是文本之间似乎根本没有设置边距。而随着文本大小的变化来增加边距通常对内容的可读性会大大提高。我们在段落之间添加了额外的控件,以便我们可以快速判断一个段落的结束位置和下一个段落的开始位置。出于这个原因,我觉得使用用户选择的根字体大小来缩放这些边距是很有意义的。
标题文本的一点扩展
**单位 em 的上岗机会。**当我们需要相对单位的时候,通常都会使用 rem,它比 em 本身这个问题更为简单,更容易被预测。也就是说 em 单位在标题和段落的页边距方面效果特别好。比如以下是使用 rem 来设置标题的样式:
h1 {
font-size: 3rem
margin-top: 6rem;
margin-bottom: 1.5rem;
}
h2 {
font-size: 2rem
margin-top: 4rem;
margin-bottom: 1rem;
}
h3 {
font-size: 1.5rem;
margin-top: 3rem;
margin-bottom: 0.75rem;
}
上面的代码看上去除了可以写成复合值之外没什么问题,但看上去还是有点“啰嗦”。虽然每个标题的级别都有自己的字体大小,我们需要为每个标题计算唯一的边距值,以下得到的效果是一样的,但简单许多:
h1 {
font-size: 3rem;
}
h2 {
font-size: 2rem;
}
h3 {
font-size: 1.5rem;
}
h1, h2, h3 {
margin-top: 2em;
margin-bottom: 0.5em;
}
每个标题级别都有一个唯一的字体大小,但是通过em,他们可以共享他们的边距声明。这是因为em是根据当前元素的字体大小计算的。
换句话说,我们说每个标题级别应该有“2x”的上边距和“0.5x”的下边距。这些比率适用于标题的字体大小。
最终,这两种方法都是 100% 有效的,并且同样可访问。😄
宽度与高度
我们再看一种场景,这里有一个固定宽度的按钮:
button {
width: 240px;
font-size: 1.25rem;
max-width: 100%;
}
我们都知道 font-size 应该以 rem 来设置,但是……它的 width 呢?这里面有两个方案:
7. 如果我们将宽度设置为240px,则按钮不会随着字体大小而增长,从而导致换行和更高的按钮。
8. 如果我们将宽度设置为15rem,按钮将随着字体大小而变宽。
选哪个?还是需要取决于实际情况! 在大多数情况下,我认为使用 rems 更有意义。这保留了按钮的比例和美观。如果按钮有一个特别长的单词,它会降低溢出的风险。但是,在某些情况下,px 可能是更好的选择。也许如果你有一个非常具体的布局,垂直空间比水平空间更丰富。
小贴士
一般来说,在设置固定宽高的时候需要非常小心。在上面的示例中width: 15rem,在许多情况下,设置会破坏移动布局,因为当用户调整默认字体大小时,它可能会为其容器生成一个很夸张的值!但我们可以通过将其限制为最大 100% 来缓解这种情况:
button {
max-width: 100%;
}
同样,当涉及高度时,我们经常希望min-height使用height. 这允许容器长到它需要的高度,以容纳它的子级元素。当用户放大他们的字体大小时,这一点变得很重要,因为文本最终会换行到更多行。
改善开发体验
rem 的换算并不容易,在正常情况下,进行换算的时候我们会得到很多小数位,比如:
- 14px → 0.875rem
- 15px → 0.9375rem
- 16px → 1rem
- 17px → 1.0625rem
- 18px → 1.125rem
- 19px → 1.1875rem
- 20px → 1.25rem
- 21px → 1.3125rem
即便你大脑的计算能力很棒,很容易就能算出这个结果,但并不是每个人都可以做到你这样,所以我们会想很多办法去改善这个问题。
62.5%
将 root 元素的根元素的 font-size设置为 62.5%,这将会在很大程度上改善你的开发体验。
html {
font-size: 62.5%;
}
p {
/* Equivalent to 18px */
font-size: 1.8rem;
}
h3 {
/* Equivalent to 21px */
font-size: 2.1rem;
}
很多人都喜欢这个方案,因为这么做了以后数字运算变得简单了很多,如果想要设置 18px 的 rem 值,只需要移动小数点的位置,并将 px 替换为 rem 即可,而不用把 18 / 16 得到 1.125 再加上 rem。
其实这种方法并不好,有几个原因, 9. 它会破坏第三方包的兼容性。如果有一个第三方的包是使用了 rem 作为单位,那么这个包中的尺寸会比原有的尺寸小 37.5%!同样的,这个行为也会破坏浏览器扩展的表现。 10. 这种方法存在这很大的迁移、重构挑战。没有渐进式的方式来逐步采用它,你只能替换项目中的每个单位的声明和值。而且还需要说服团队中的其他成员,说服他们说这是值得的,但这对很多团队来说并不总是行得通的。
CSS calc() 计算
p {
/* Produces 1.125rem. Equivalent to 18px */
font-size: calc(18 / 16 * 1rem);
}
h3 {
/* Produces 1.3125rem. Equivalent to 21px */
font-size: calc(21 / 16 * 1rem);
}
h2 {
/* Produces 1.5rem. Equivalent to 24px */
font-size: calc(24 / 16 * 1rem);
}
h1 {
/* Produces 2rem. Equivalent to 32px */
font-size: calc(32 / 16 * 1rem);
}
使用 CSS calc() 方法可以将像素值转换为 rem。这很棒,但是它要写的代码是在太多了……
CSS var() 变量
我觉得这是最好的办法了:
html {
--14px: 0.875rem;
--15px: 0.9375rem;
--16px: 1rem;
--17px: 1.0625rem;
--18px: 1.125rem;
--19px: 1.1875rem;
--20px: 1.25rem;
--21px: 1.3125rem;
}
h1 {
font-size: var(--21px);
}
我们可以一次性完成所有的计算,并且用 CSS 变量来存储这些选项值,随用随取,几乎就和只用 px 值一样。虽然说以数字开头的 CSS 变量看上去不太规范,但它是符合规范的,并且适用于所有现代浏览器。
如果项目中还有着栅格系统的设计,我们也可以使用相同的技巧:
html {
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-md: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.3125rem;
--font-size-2xl: 1.5rem;
--font-size-3xl: 2.652rem;
--font-size-4xl: 4rem;
}
当然了,我们每个人都会有着自己的想法,在对技术做出选择的时候,会有着各自项目、团队的考量,但是这没关系,哪种方式都可以使用,最重要的是保证好的用户体验,只要他们使用的时候没有埋怨和不适,那你就做对了。💯
以上。