前端开发者学堂 - fedev.cn

聊聊重置 CSS 那些事儿

发布于 大漠

重置CSS (Reset CSS) 最近因 @Elad 发布的最新版本而又热闹起来。加上我又热衷于 CSS,只要是有关于 CSS 最有趣、最新、最实用的方面,我都希望能第一时间在国内的社区呈现(分享)。那么问题来了,重置 CSS 已是很古老的话题了,在今天有啥好聊的呢?原本我也是这么想的,但 @Elad 最新版本的重置 CSS 项目,它还是非常有意思的,我觉得有必要拿出来和大家一起分享,或者说聊聊这里面有关于 CSS 方面的新特性。如果你对这方面的话题感兴趣(想一探究竟),那么接下来的内容值得你花点时间阅读。

重置 CSS 的发展进程

作为 Web 开发者(特别是CSSer)对于 重置 CSS (Reset CSS) 并不会感到陌生,而且在社区和团队都有着不同的版本的重置 CSS,最为粗暴的应该是下面这种样的:

* {
    margin: 0;
    padding: 0;
}

或者是:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

*::before,
*::after {
    box-sizing: border-box;
}

我想,在社区中有很多团队都是以这种粗暴方式来设置重置 CSS 的,但作为一名专业的 CSSer而言,是非常反感这种姿势的!

抛开这种粗暴的重置CSS不说,在整个社区(在 CSS 发展历程)中,最早的一份重置 CSS 样式表是由 @meyerweb 编写的,因此也常称为 Eric Meyer CSS Reset,也简称为 CSS Reset

随着 Web 技术不断的革新,Eric Meyer 的重置 CSS 也被冠上了“硬性” 的代名词,那是因为,他说在大多数情况下,我们不需要浏览器的基本样式,比如我们从 <h1><h6> 等元素得到的字号大小,或者<ul><ol>列表元素的默认样式。例如,我们使用列表只是为了语义,而且因为它在其他方面有助于 Web 的可访问性(A11Y)和 SEO。

因此,@Nicolas Gallagher 提出了一套更温和的重置 CSS,即 Normalize.css

他修复了不同浏览器中的实现差异。而且至今,它是很多 Web 项目必备的一个 CSS 样式表。而且很多优秀团队都在使用这个重置 CSS 样式表,比如说 Twitter、Github、Medium等,而且很多小团队也在使用该重置样式表(说不定你应该也还在使用,对吧)。

话又说回来了,为什么normalize.css 这么受欢迎呢?那是因为,它除了具备 “Eric Meyer CSS Reset” 特性之外,还重围了一些 DOM Shadown 相关的 CSS,比如 ::-moz-focus-inner::-webkit-file-upload-button等。

除此之外,你也有可能像我一样,两个重置 CSS 样式表都不使用,更喜欢根据自己团队或自己业务所需,定制一个更符合自己的重置 CSS。也有可能,你更喜欢所 Normalize.css 和 CSS Reset(Eric Meyer CSS Reset)结合使用。不过,这都不是最重要的,重要的是你有一个更符合自己的重置 CSS。

虽然说今天是 2021年(也马上就到2022年),但 Normalize.css 和 CSS Reset 都可以满足我们重置浏览器样式的需求。那么问题来了,为什么 @Elad重构一个新的重置CSS(被称为New CSS Reset)呢?

为了便于区分,我更喜欢把 @Elad 重构的重置 CSS 称为 Elad CSS Reset!

正如上图所示,Elad CSS Reset 中有很多新的 CSS 特性,这些新的 CSS 特性可以让我们创建一个更有效的重置CSS,同时也具备以往重置 CSS 相似的效果。

你可能和我一样,会对这里面使用到的一些新特性感到好奇,或者想一探究竟吧。如果是,那么花时间阅读后面的内容是非常值得的!

为什么需要重置 CSS

如果你是一位 Web 开发者,你应该听说过,有一个样式表叫客户端样式表(User Agent Stylesheet),而这个客户端指的是“浏览器”(对于Web开发者而言,客户端大多数情况之下指的是浏览器)。默认情况之下,不同的浏览器对 HTML 的默认样式的解析是有的差异的,比如说 <p> 元素,他在 Chrome 浏览器有这样一段默认的样式规则:

p {
    display: block;
    margin-block-start: 1em;
    margin-block-end: 1em;
    margin-inline-start: 0px;
    margin-inline-end: 0px;
}

但这并不意味着,所有客户端(浏览器)对所有 HTML 元素的默认样式解析都是一样的。换句话说,在一个完美的世界中,每个浏览器都应该以完全相同的方式解析或应用所有的 CSS 规则。然而,在现实世界中并不是一个完美的世界,许多 HTML 的元素的 CSS 样式规则(默认样式规则)在不同的浏览器是解析是不一样的。

@Jens Oliver Meiert 整理了一份不同的浏览器的用户代理样式表的清单,如果你对此感兴趣的话,可以点击这里查阅

正基于这个原因,重置CSS孕育而生,它起着重要的作用:从页面元素中删除默认的样式,这样你就可以用你选择应用的属性“重新开始”。这很重要,有两个原因:

  • 将所有的浏览器放在一个公平的竞争环境中。不同的浏览器对元素应用不同的默认样式,所以你想让你的网站在所有不同的浏览器上看起来都一样,重置CSS就显得很重要
  • 它允许你在应用页面元素的marginpadding等属性时“向前思考”。在从元素中删除属性时,不必“逆向思考”,而只能将它们应用于你知道需要它们的元素

简而言之,重置CSS可以让所有浏览器对HTML的元素默认样式采用一样的解析!(当然,重置CSS会有遗漏,但大部分趋于一致,这也是重置CSS存在的目的)。

也就是说,使用一套精心设计的全局性的 重置CSS 样式,使设计者(Web 开发者)能够对浏览器的默认行为做出假设。这些假设极大地简化了只使用一套 CSS 规则来创建“普便”(能覆盖众多主流浏览器)一致的CSS设计的过程。这种过程的简化极大地节省了时间和金钱。许多业界的顶级设计师已(Web 开发者)经使用重置 CSS 多年,收获了不少成果。正如 @Jeff Starr 的《Killer Collection of CSS Resets》一文中所收集的全局重置 CSS 样式表。也从侧面说明了不同的场合,不同的团队使用了这些重置样式,并取得了一定的作用,甚至是你在此基础上构建出一个混合体,来满足自己开发所需。换句话说,这也是从侧面验证了重置 CSS 的必要性。

你可能不完全接受重置 CSS,但在一个团队中协同开发时,重置 CSS 还是有一定必要的。

聊聊 Elad 的重置CSS

@Elad 的重置 CSS 所起的作用和 Normalize.css 以及 Eric Meyer CSS Reset 是等同的,不同的是使用了一些新的 CSS 特性。别的不先说,先上代码吧:

/* 删除除 `display` 属性之外的客户端所有默认样式;symbol * 是为了解决 Firefox 浏览器中 SVG 雪碧图的 Bug */
*:where(:not(iframe, canvas, img, svg, video):not(svg *, symbol *)) {
    all: unset;
    display: revert;
}

/* box-sizing 的首选值*/
*,
*::before,
*::after {
    box-sizing: border-box;
}

/* 删除列表项默认标记符样式 */
ol, ul {
    list-style: none;
}

/* 让图片不超出容器宽度 */
img {
    max-width: 100%;
}

/* 删除表格单元格之间的间距 */
table {
    border-collapse: collapse;
}

/* 使用 revert 恢复 Safari 浏览器 textarea 元素的 white-space 属性值 */
textarea {
    white-space: revert;
}

上面的代码中有几个关键词,估计会令很多 Web 开发者感到陌生,比如 :where():not()allunsetrevert等。这些关键词是:

  • 全局性的 CSS 重置关键词 unsetrevert
  • 新的 CSS 属性 all 可以重置所有属性的组合
  • :where() 伪类选择器,消除 CSS 选择器权重
  • 带有多个参数的 :not()伪类选择器

从代码上来看,Elad 重置CSS想做的事情是 display 属性之外,我们不想使用任何客户端给HTML元素设置的默认样式。换句话说,这个重置CSS删除了我们在特定 HTML 元素(除<iframe><canvas><img><svg><video>这样的特殊元素)上得到的所有默认样式(display属性之外)。

如果你想恢复特定 HTML 元素的浏览器的默认样式,可以像下面代码这样来恢复:

input[type="checkbox"],
input[type="radio"] {
    all: revert;
}

即使用 all:revert可以恢复元素在浏览器中的默认样式。为什么呢?这都是后话(后面我们会提到)。

CSS 中的 all 属性

CSS 的 all 属性出自于 CSS Cascading and Inheritance Level 4,它可以重置除了directionunicode-bidi 之外的所有 CSS 属性。该属性的值为 initialinheritunsetrevert

  • initial:改变该元素或其父元素的所有属性的值为 initial
  • inherit:改变该元素或其父元素的所有属性的值至他们的父元素属性的值(继承父元素的属性值)
  • unset:如果该元素的属性的值是可继承的,则改变该元素或该元素的父元素的所有属性的值为他们的父元素的属性值,反之则改变为初始值
  • revert:指定依赖于声明所属的样式表原点的行为
    • “User-agent Origin”,相当于 unset
    • “User Origin”,将层叠回滚到用户代理级别,以便计算指定的值,就好像没有为该元素指定作者级别或用户级别规则
    • “Author Origin”,将层叠回滚到用户级别,以便计算指定的值,就好像没有为元素指定作者级规则。出于 revert 的目的,“作者”原点包括“覆盖”和“动画”原点

在 CSS 中,有两组属性:

  • 继承的属性组:默认具有继承性的属性,主要是排版属性,比如 font-sizefont-family
  • 非继承的属性组:默认不继承的属性,比如用于盒模型的 paddingmarginborder

在规范的每一个属性的介绍中,都会有一个 "inherited" 项,如果值为 "yes",表示该属性是一个可继承的属性;如果值为 “no”,表示该属性是一个不可继承的属性。

有时候在 CSS 中试图重置一个属性,但又希望保持继承的行为,比如像排版相关的属性。那么就可以使用inherit关键词:

font-size: inherit;  
line-height: inherit;
color: inherit;

对于非继承属性组中的其他属性,我们希望在大多数情况下得到它们的初始值(即initial)。值得一提的是,对于不同的属性,initial的计算方式是不同的。

max-width: initial; /* = none */ 
width: initial;     /* auto */
position: initial;  /* = static */

同样的,在规范中介绍每个属性时,会有一个“Initial”的选项,对应的值就表示该属性的初始值。比如上面示例代码中的 max-widthwidthposition在属性介绍中,可以看到他们的初始值分别是 noneautostatic

有关于 CSS 的 inheritinitial 更详细的介绍可以阅读《图解CSS:CSS层叠和继承》一文!

如果你明白了 CSS 属性的 inheritinitial 关键词值的基本原理之后,那么就会明白,如果我们想重置所有的 CSS 属性就不能直接使用 all 属性。这是因为,如果我们把所有属性都重置为initial,即 all:initial时,就会失去对继承属性组的固有行为。而假设我们用继承值重置所有属性,即 all:inherit,则所有的属性都得到了继承,包括不具有继承特性的属性,比如盒模型中的相关属性。

我们来看一个简单的示例,比如我们有一个这样的 HTML 结构:

<!-- HTML -->
<body>
    <div>
        Hello! <strong>W3cplus!</strong>
    </div>
</body>

然后按照平时写 CSS 的习惯来为上面的 HTML 元素添加一些基本样式:

body {
    padding: 2vw; /* initial = 0, inherited = no*/
    background-color: #557;/* initial = tansparent, inherited = no */
}

div {
    background-color: #f36; /* initial = tansparent, inherited = no */
    color: #fff; /* initial = depends on user agent, inherited = yes */
    font-size: 2rem; /* initial = medium, inherited = yes*/
    margin: 2vw; /*initial = 0, inherited = no*/
}

strong {
    font-size: 3rem;/* initial = medium, inherited = yes*/
}

你在浏览器中看到的效果如下:

<body><div><strong> 元素中分别使用了 background-colorpaddingcolorfont-sizemargin 几个 CSS 属性,其中 background-colorpaddingmargin非继承属性(“Inherited = no”),而 colorfont-size继承属性("Inherited = yes")。并且这三个都还有客户端浏览器提供的样式

/*User Agent Stylesheet*/
body {
    display: block;
    margin: 8px;
}

div {
    display: block;
}

strong {
    font-weight: bold;
}

而且 strong 继承了父元素 divcolor属性的值。如果我们在 divstrong 两个元素中同时设置 all: inherit时,得到的效果和前面的完全不一样:

此时,divstrong 重置了当初自己设置的属性,并且继承了各自父元素的一些属性:

  • div 元素继承了 body 元素的 paddingbackground-color,同时也继承了body代理客户端的样式colormargindisplayfont-sizefont-family
  • strong元素继承了 div 元素的样式,包括div继承来自 body和客户端的样式

如果你在 divstrong 设置all: initial时,效果如下:

此时,divstrong的样式都重置到了对应的初始样式,也就是对应属性的默认样式,包括代理客户端也重置为对应属性的初始值。

最后再来看,将 divstrongall 设置为 unset。则相当于根据属性的类型来重置属性,如果它是一个继承属性,则等于 inherit;如果它是一个非继承属性,则等于initial,包括来自代理客户端的样式,即:

div {
    background-color: unset; /* initial = tansparent */
    color: unset; /*  inherited = yes,获取其父元素的 color */
    font-size: unset; /* inherited = yes,获取其父元素的font-size*/
    margin: unset; /*initial = 0*/
}

strong {
    font-size: 3rem;/* inherited = yes,获取其父元素的font-size*/
}

/*User Agent Stylesheet*/

div {
    display: unset; /*initial = inline*/
}

strong {
    font-weight: unset; /*inherit = yes, 继承其父元素的 font-weight*/
}

所以你将看到的效果如下:

我想你现在已经知道,在重置 CSS 中设置 all:unset 的作用了吧:

  • 将所有继承的属性重置为承继值(inherit
  • 将所有非继承的属性重置为初始值(initial

为此可以使用通配符选择器(*),设置all 属性的值为unset

* {
    all: unset;
}

CSS 的 revert 属性值

事实上,把所有元素都设置为 all: unset并不能代表一切都是 OK。也就是说,你可能并不希望所有 CSS 属性都按照 inheritinitial来设置值。比如 display 属性。

CSS 中的 display 属性其初始值是 inline,而且是不可继承的属性:

但在 HTML 中,元素又分为 块元素(比如 div)、内联元素(比如 span)。块元素和内联元素最大的差异是:

  • 块元素的 display 的值为 block
  • 内联元素的 display 的值为 inline

至少用户代理客户端(比如浏览器)就是这么实现的,即块元素和内联元素对应的 “User Agent Stylesheet” 的 display值分别为 blockinline

div {
    display: block;
}

内联元素在 display 属性采用的就是其初始值 inline

稍微熟悉 CSS 的同学应该知道,在 CSS 中有一个非常重要的概念,那就是 视觉格式化模型 ,该模型主要是用来计算元素盒子位置。简单地说,不同盒子使用的是不同格式化上下文(Formatting Context)来布局,每个格式化上下文都拥有自己不同的渲染规则,而这些规则是用来决定其子元素如何定位,以及和其他元素的关系。好比水倒进不同的器皿中,会有不同的形态:

all: unset 也就重置了所有元素的盒子只有一种格式化上下文,即内联格式化上下文:

这并不是我们想要的,或者说大部分情况之下是不相要的。为此,在设置 all: unset 的情况之下,需要重置 displayrevert

* {
    all: unset;
    display: revert;
}

现在,为了理解独特的 revert 关键词对 display 的作用,我们需要先从浏览器得到的两种类型谈起。我们从浏览器得到的样式是由两层组成的:

  • 第一层是 CSS 的初始值,指的就是 CSS 中所有属性的初始值,包括一些属性的继承行为。比如 div 元素,他的 display 初始值是 inline(事实上,所有元素的 display 都是 inline
  • 第二层是用户代理样式表(User Agent Stylesheet),这些是由浏览器为特定的 HTML 元素定义的样式,比如 div元素,他的 display 的值为 block,而 lidisplay 值为 list-item

前面也说过,使用重置CSS的目的或初衷是覆盖客户端给 HTML 元素设置的默认样式,也就是要覆盖第二层的基本样式。而当我们 使用 all:unset进行重置时,会删除用户代理样式表的所有样式

注意,CSS 的 display 有点特殊

正如我们已经看到的,CSS 中的每个属性都有一个初始值("Intinal"对应的值)。这就意味着,如果我们将 display 属性重置为 intinal 值,那么像 <div> 元素或其他 HTML 中的块元素(比如h1),它们的 display 属性值部是 inline。这是因为 CSS 中的 display的初始值为 inline,而且不分 HTML 元素

只不过,浏览器为了区分块元素和内联元素在浏览器中的渲染行为有所差异,用户代理客户端会把块元素的 display 都设置为 block,而像 <li> 会设置display值为list-item。对于原本的行内元素,比如 span元素,浏览器客户端样式依旧会让其 display 的值为初始值,即 inline。也正因此,在浏览器中渲染块元素或列表项元素时,display的值会改变成 blocklist-item

div { 
    display: unset; /* = inline */ 
}

span { 
    display: unset; /* = inline */ 
}

table { 
    display: unset; /* = inline */ 
}

li {
    display: unset; /* = inline */
}

当然,这些行为并不是我们所需要的。display 属性是我们想从浏览器中得到的唯一例外。正因为如此,我们使用独特的关键词 revert 来从用户代理样式表中带回默认的 display值。也就是说,元素设置displayrevert,那么对于块元素,他对应的display就是block,对于列表项元素,他对应的 display就是list-item

/*User Agent Stylesheet*/
div { 
    display: revert; /* = block */ 
}

span { 
    display: revert; /* = inline */ 
}

table { 
    display: revert; /* = table */ 
}

li {
    display: revert; /* = list-item */
}

CSS中的 revert 值和 initialinherit以及unset是属于同一范畴(出自于 CSS Cascading and Inheritance Level 4)。而且 revert的值是 独一无二的。首先,它检查用户代理样式表中是否有一个特定属性的默认样式,用于它所在的特定HTML元素,如果它找到了,它就采用它,比如 div 元素的 display,会取blockli元素的display会取list-item。如果在用户代理样式表中没有找到,那么 revert的表现就像 unset一样,这就意味着,如果该属性是一个默认的继承属性,它就使用inherit;如果不是,它就使用initial

CSS 的 :where():not() 选择器

*:where(:not(iframe, canvas, img, svg, video):not(svg *, symbol *)) {
    all: unset;
    display: revert;
}

选择器中使用了 Selectors Level 4 规范中的 :where():not() 选择器。

  • :not() 用来匹配不符合一组选择器的元素,由于它的作用是防止特定的元素被选中,它也被称为反选伪类。比如 :not(p),表示选中所有不是p的元素
  • :where() 将会选择所有能被该选择器列表中任何一条规则选中的元素,它是用来降低选择器权重的,即 :where() 的权重总是0

*:where(:not(iframe, canvas, img, svg, video):not(svg *, symbol *)) 选择器简化一下,把:where()忽略,变成:

*:not(iframe, canvas, img, svg, video):not(svg *, symbol *) {
    
}

在一个选择器中出现多个:not()时,比如:not():not(),表示既不也不。比如 :not(div):not(span),表示选中的既不是<div>元素,也不是 <span> 元素。回到示例中的话,则给示选中的既不是 <iframe><canvas><img><svg><video> 元素,也不选中 svg *(表示 svg 元素的所有后代元素)、symbol *(表示 symbol元素的所有后代元素) 元素。而CSS的通配符选择器*则表示选中文档中的所有元素。如此一来,结合起来的选择器则表示选中文档中所有元素,但又删除了iframecanvasimgsvgvideosvg *symbol * 元素,即 选中HTML文档中除 <iframe><canvas><img><svg><video><svg>的所有后代元素和<symbol>的所有后代元素之外的所有元素!

前面我们提到过,重置HTML文档中所有元素的所有样式时,我们选择了 *{all:unset}。不过,这样做会破坏那些可以通过widthheight属性获得尺寸的元素,比如 <iframe><canvas><img><svg><video>等元素。不幸的是,这样重置一切时,这些元素的widthheight是由auto值定义。这样做虽然很强大,但也会消除元素的widthheight属性的影响。

这可能是个问题,因为在我们通过 HTML 的 widthheight 属性设置元素尺寸的情况下,我们希望准确的尺寸来自 HTML 元素。我们倾向于从 HTML 中获取,而不是从 CSS 中获取,因为当它来自 CSS 时,可能会在页面渲染的时候触发布局偏移。

有关于 <img> 未显式设置 widthheight 属性值可能会触发布局偏移,造成而面的重排和重绘,影响 Web 渲染性能等。有关于这方面更详细的介绍,还可以阅读《聊聊 img 元素》和《图片的优化》两篇文章。

如果你想去除所有这些特定元素的重置效果的唯一方法是把它们放在 :not() 选择器中。也正基于这个原因,才会有这样的选择器和CSS规则:

*:not(iframe, canvas, img, svg, video):not(svg *, symbol *) {
    all: unset;
    display: revert;
}

不过 @Elad 的重置 CSS中的这段代码还结合了 :where() 选择器,即:

*:where(:not(iframe, canvas, img, svg, video):not(svg *, symbol *)) {
    all: unset;
    display: revert;
}

这样做的原因又是为何呢?在回答之前,先简单的来了解 CSS 选择器:where() 的作用和背后思路。CSS 的选择器 :where() 主要作用是 用来降低选择器权重的,简单地说,带有:where()选择器,权重都是0

比如:

img {}
:not(img) {}
:where(:not(img)) {}
*:not(img){}
*:where(:not(img)){}

使用CSS选择器权重计算器,可以看到,带有:where()选择器权重最低,为 0

比如下面这个示例:

a:not(:hover) {
    text-decoration: none;
}

nav a {
    text-decoration: underline; /*并未生效*/
}

事实上,nav a 并未生效,这是因为nav a选择器权重(0, 0, 2)比 a:not(:hover)选择器权重(0, 1, 1)要低:

但如果在a:not(:hover)上加上:where()则不同:

a:where(:not(:hover)) {
    text-decoration: none;
}

nav a {
    text-decoration: underline; /* 生效*/
}

回到重置CSS中的选择器来:

*:not(iframe, canvas, img, svg, video):not(svg *, symbol *)         » 0, 0, 2
*:where(:not(iframe, canvas, img, svg, video):not(svg *, symbol *)) » 0, 0, 0

这样一来,我们要重置元素样式时,任何选择器权重都要高于重置CSS中的,比如:

*:where(:not(iframe, canvas, img, svg, video):not(svg *, symbol *)) {
    all: unset;
}

div {
    font-size: 2rem;
}

重置盒模型

从 《CSS 盒模型》 一文中,我们知道在 CSS 中任何一个元素都有对应的一个盒子,而且每个盒子都有大小,其大小的计算都和盒模型的属性有紧密关联,比如 paddingborder 等。

CSS 逻辑属性出现之后,在 CSS 中定义盒模型的属性除了我们熟悉的物理属性之外,还新增了相应的逻辑属性,有关于这方面的详细介绍,可以阅读《CSS的逻辑属性对盒模型带来的变化》一文。

而稍微了解 Web 历史的同学都应该还记得,CSS 盒模型计算至少有两种差异:

  • 定义盒子尺寸的 widthheight 包括 paddingborder,即 paddingborder 在盒子内部
  • 定义盒子尺寸的 widthheight 不包括 paddingborder,即 paddingborder 在盒子外部

另外,CSS 的盒子有不同的参考框,比如 content-boxpadding-boxborder-boxmargin-box

同时,在 W3C 的 CSS Box Sizing Module Level 3 使用 box-sizing 来定义盒子模型的参考框,只不过,它只有content-boxborder-box,并且这两种不同的参考框对盒子尺寸大小计算是不同的:

从规范中我们可以获知,box-sizing 属性的初始值("initial")是 content-box,并且该属性是一个非继承属性。按照前面所介绍的,在重置 CSS 中显式设置了 all: unset,也代表着 box-sizing 会取初始值 content-box。只不过,在现代 Web 开发中,你几乎看到的任何项目都将所有元素的box-sizing重置为border-box。或者说,你在很多项目的中会看到像下面这样的 CSS 代码,甚至是在一些重置 CSS 代码中:

html {
    box-sizing: border-box;
}

*,
*::before,
*::after {
    box-sizing: inherit;
}

在所有元素和其伪元素使用 box-sizing: inherit 来继承 htmlbox-sizing: border-box是来自于 @Jon Neal的,他说:

这将给你带来同样的结果,并使你更容易改变元素中的盒子大小。

也就是说,如果你想在另一个元素中重置box-sizingcontent时,可以像下面这样:

select {
    box-sizing: content-box;
}

就我个人而言,我也更喜欢将 box-sizing重置为border-box,主要是更符合我们计算的方式。按常理来说,如果你一个元素的盒子设置了 width200px,那么对于盒子的paddingborder设置什么值,盒子尺寸的宽度都应该是 200px,而不应该是其他的。

你可能会好奇的问。既然这么多人(这么多网站或应用)都更喜欢将 box-sizing重置为 border-box(不喜欢其初始模式content-box),那么为什么浏览器引擎自身不去修复呢?或者说,W3C相关的规范不去重新定义box-sizing的初始值为border-box呢?出于这个原因,主要是这样粗暴做修复,那么旧网站在浏览器中的渲染就会被破坏。但对于新开发的网站或应用,你人为重置 box-sizingborder-box 并不会有这种可现出现。

同样的,@Elad 的重置CSS 也将其纳入到里面,即:

*,
*::before,
*::after {
    box-sizing: border-box;
}

删除列表项和折叠边框样式

在 @Eric Meyer 重置 CSS 中,对有序列表(ol)和无序列表(ul)的列表项默认符号和表格单元格边框样式折叠都做了相应处理:

ol, ul {
    list-style: none;
}

table {
    boder-collapse: collapse;
}

抛开 table 不说(毕竟现在都是移动端上开发为主,几乎用不上表格元素),我们在开发 Web 的时候,如果会用到 olul 都是希望是一个列表。从语义化角度来说,浏览器渲染他们就应该有相应的列表项符号。从这个角度来说,不应该一刀切的将列表项默认样式符删除,即 list-style 设置为 none。不过,有的时候,我们并不希望列表项在浏览器中渲染出默认的列表项符号(比如导航,菜单等)。这样一来,就出现争议了,列表项的list-style该不该被移除。

如果我们选择不重置列表项样式,这意味着我们在构建像导航和菜单这样的组件时,就不能使用,因为你在构建组件的时候,还是要显式的删除列表项符号,即:

.nav {
    list-style: none
}

这也意味着,语义化标签在 HTML 的使用大打折扣。事实上呢?我们在开发 Web页面或Web应用时,都一直提倡要开发一个更具可访问性的Web,即提倡使用具有语义化的 HTML 标签来构建 Web。如果从这两个方面来考虑的话,我更推荐使用具有语义化的标签。或者说,我们可以对 @Elad 的重置 CSS 稍作改变:

ul[role="list"],
ol[role="list"] {
    list-style: none;
}

role是 ARIA 中重要属性,用来定义元素的角色。事实上,在 HTML 中的每个元素都有自己的 role,具体的可以在 ARIA In HTML 中查阅。

img 设置max-width100%

从 《聊聊 img 元素》和《图片的优化》以及《响应式图片使用指南》等一系列文章中知道,可以在 img 元素上显式设置 max-width100%,可以让图片响应容器大小。为此,在重置CSS的时候会显式设置:

img {
    max-width: 100%;
}

但这并不是最佳的。不知道你是否还记得,img元素不显式设置display为非inline的值或显式设置vertical-align的值,img会产生个差不多 3px的空边下边距:

为了避免这种现象,我们应该在重置CSS中的img显式设置displayblockvertical-aligntop这样的样式,不过我更趋向于显式设置displayblock

img {
    max-width: 100%;
    display: block;
}

为了减少 <img> 标签引起 Web 布局的抖动,因此提倡在 <img> 元素上显式设置 widthheight,但随着 CSS 的 aspect-ratio出现,我们还可以使用标签元素的宽高比,结合max-width重新计算图片宽高。也能减少布局的抖动,为此,重置img的更好方式是:

img {
    max-width: 100%;
    height: auto;
    display: block;
    aspect-ratio: attr(width) / attr(height)
}

注意,attr() 函数在 aspect-ratio 属性中还未得到支持,但可以使用 aspect-ratio: 400 / 300,其中 400300 对应 <img>widthheight值。

重置CSS还可以加些什么

上面几点是 @Elad 重构建的重置 CSS 的特色之处。我们并不想花太多时间拿他和以往的重置CSS做优略对比,因为没有最好的,只有最合适的。如果是我来构建的话,我还会在该基础上新增一些其他的重置样式,比如:

:root {
    color-scheme: light dark;
}

* {
    -webkit-tap-highlight-color: transparent;
}

html {
    width: 100%;
    height: 100%;
}

html:focus-within {
    scroll-behavior: smooth;
}

body {
    min-height: 100%;
    font-family: system-ui;
}

a {
    text-decoration-skip-ink: auto;
}

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .01ms !important;
        scroll-behavior: auto !important;
    }
}

简单地介绍一下:

  • color-scheme 属性可以通过 CSS 对用户代理的自动颜色进行控制,目的是处理用户的偏好设备,比如暗黑模式,对比度调整或特定的主题配置方案等。详细可以阅读《系统偏好设置的那些事儿
  • scroll-behavior 取值为 smooth 可以让页面滚动更平滑,一般将该属性设置在 html 元素上。示例中的代码还采用了 CSS 的伪类选择器 :focus-within,表示当 html 元素本身或其后代元素获得焦点时,scroll-behavior的值为 smooth。在 CSS 中除了该属性,还有其他有关于优化滚动方面的属性,比如 pull-to-refresh滚动捕捉
  • font-family: system-ui 设置字体为系统字体,详细可以阅读《Web Fonts vs. 系统字体
  • prefers-reduced-motion新媒体查询中的一个特性,也可以根据用户偏好设备来设置不同的样式规则。比如有些用户会选择禁用动画播放,因为有些动效会引起用户的不适。
  • htmlwidthheight显式设置值为100%,这里为会么没有选择视窗单位(100vw100vh),主要还是因为,在一些浏览器中会造成一定的问题,比如,引起滚动条的出现。详细的介绍可以阅读《Avoid 100vh On Mobile Web》和《CSS fix for 100vh in mobile WebKit
  • text-decoration-skip-ink可以指定在经过字形上升线和下降线时如何绘制上划线和下划线,取值为 auto时,浏览器可以中断上划线和上划线,使它们不接触或不接近字形。也就是说,它们会在跨越字形的地方被打断
  • -webkit-tap-highlight-color 是一个没有标准化的属性,能够设置点击链接的时候出现的高亮颜色。显示给用户的高光是他们成功点击的标识,以及暗示了他们点击的元素。但很多时候,并不希望这种高亮颜色显示出来,因此将所有元素高亮色重置为transparent,让用户看不到

如此一来,你可能会有这样的一个重置CSS:

:root {
    color-scheme: light dark;
}

* {
    -webkit-tap-highlight-color: transparent;
}

*:where(:not(iframe, canvas, img, svg, video):not(svg *, symbol *)) {
    all: unset;
    display: revert;
}

*,
*::before,
*::after {
    box-sizing: border-box;
}

html {
    width: 100%;
    height: 100%;
}

html:focus-within {
    scroll-behavior: smooth;
}

body {
    min-height: 100%;
    font-family: system-ui;
}

ul[role="list"],
ol[role="list"] {
    list-style: none;
}

a {
    text-decoration-skip-ink: auto;
}

img {
    max-width: 100%;
    height: auto;
    display: block;
    aspect-ratio: attr(width) / attr(height)
}

textarea {
    white-space: revert;
}

table {
    border-collapse: collapse;
}

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .01ms !important;
        scroll-behavior: auto !important;
    }
}

当然,你可能还有更适合自己一套重置CSS样式表。在你的重置样式表中也可能有很多独特的特性或很值得学习的地方,如果有的话,欢迎一起分享。

未来特性对重置 CSS 带来的变化

不知道你是否有关注过今年 TPAC 会议上 @Miriam Suzanne 带来的话题《Improving CSS Architecture with Cascade Layers, Container Queries, Scope》。这里提出了很多新概念,比如 级联层(Cascade Layers)、容器查询(Container Queries)和 作用域(Scope)。那么这些新特性,特别是其中的“级联层”会对下一代重置CSS带来变化?或者说会影响到下一代重置 CSS 吗?为什么会有这么一问呢?

那是,因为级联层(Cascade Layers)模块是一个非常令人兴奋的 CSS 特性。正如 @Bramus 所说

With Cascade Layers coming, we developers will have more tools available to control the Cascade. The true power of Cascade Layers comes from its unique position in the Cascade: before Selector Specificity and Order Of Appearance. Because of that we don’t need to worry about the Selector Specificity of the CSS that is used in other Layers, nor about the order in which we load CSS into these Layers — something that will come in very handy for larger teams or when loading in third-party CSS.

大致意思是说:“随着级联层的到来,开发者将有更多的工具来控制级联(Cascade)。级联层的真正力量来自于它在级联中的独特位置,即在选择器权重和外观顺序之前。正因为如此,我们不需要担心在其他层中使用的 CSS 选择器权重,也不需要担心将 CSS 加载到这些层中听顺序。这种特性在大型团队协作中或加载第三方CSS非常有用”。

说其强大,那是因为一个“较高”(Higher)的层可以从字面上击败一个传统上权重较大的选择器,即使该层中的选择器权重很低。比如下面这个示例:

/* @layer会覆盖没有 @layer的样式 */
@layer override {
    h1 {
        color: green;
    }

    h1::before {
        content: "✔";
    }
}

/* 尽管选择器权重大于@layer中的选择器,但还是被 @layer规则覆盖 */
#h1 {
    color: red;
}

#h1::before {
    content: "✘";
}

这主要是因为 CSS 根本不在一个层中,所以分层的 CSS 赢了(@layer规则胜出),即使选择器权重更低。而且你并不局限于一个层。你可以定义它们,并随心所欲地使用它们。

@layer reset;     /* 创建第一层,并且命名为 “reset” */
@layer base;      /* 创建第二层,并且命名为 “base” */
@layer theme;     /* 创建第三层,并且命名为 “theme” */
@layer utilities; /* 创建第四层,并且命名为 “utilities” */

/* 上面代码可以简写成
@layer reset, base, theme, utilities; 
*/


@layer reset { /* 运用到 “reset” 层的样式规则 */
    /* ... */
}

@layer theme { /* 运用到 “theme” 层的样式规则 */
    /* ... */
}

@layer base { /* 运用到 “base” 层的样式规则  */
    /* ... */
}

@layer theme { /* 运用到 “theme” 层的样式规则 */
    /* ... */
}

越在底层的样式规则总是胜过顶层的样式规则。这也意味着,有了@layer之后,像:where()删除选择器权重的特性就会变得没有多大的意义了。

上面演示的是 @layer 可能会对重置CSS 产生的变化。除此之外,可能 @scope 也会对重置CSS带来影响。说实话,我从未想过,因为我自己现在对 @layer@scope都还不怎么理解。我只知道他们是非常强大的CSS的特性,特别是在编写CSS框架的时候,会给CSS带来质的变化。

嗯,现在你和我一样,只需要知道他们非常强大即可。因为后面我们可以一起进入到这个领域,深度探讨他们。如果你感兴趣的话,欢迎持续关注后续的相关更新。最后,希望这段重置CSS对你有所帮助,同时也能帮助你了解一些新的 CSS 特性。