将 CSS 自定义属性从 :root 中分离出来可能是个好主意

Avatar of Kevin Powell
Kevin Powell

DigitalOcean 为您的旅程的每个阶段提供云产品。从 $200 免费积分开始!

CSS 自定义属性已经成为一个热门话题,现在有很多关于它们的优秀文章,从 优秀的关于它们如何工作的入门 到创意教程,再到 用它们实现一些真正的魔法。如果您已经阅读了关于该主题的一两篇文章,那么我相信您已经注意到,他们会在 99% 的情况下首先在 :root 上设置自定义属性。

虽然将自定义属性放在 :root 上对于您需要在整个网站中使用的元素非常棒,但有时将自定义属性本地化更有意义。

在本文中,我们将探讨

  • 为什么我们一开始将自定义属性放在 :root 上。
  • 为什么全局作用域不适合所有情况。
  • 如何使用本地作用域的自定义属性来克服类冲突。

自定义属性和 :root 的关系如何?

在我们深入探讨全局作用域之前,我认为有必要了解为什么每个人都首先在 :root 上设置自定义属性。

我一直都在 :root 上声明自定义属性,甚至没有多想。几乎每个人都在做这件事,甚至没有提到原因,包括 官方规范

当真正提到 :root 的时候,它会提到 :roothtml 相同,但具有更高的特异性,仅此而已。

但是,这种更高的特异性真的重要吗?

并不重要。它所做的只是以更高的特异性选择 html,就像类选择器在选择 div 时比元素选择器具有更高的特异性一样。

:root {
  --color: red;
}

html {
  --color: blue;
}

.example {
  background: var(--color);
  /* Will be red because of :root's higher specificity */
}

建议使用 :root 的主要原因是,CSS 不仅用于为 HTML 文档设置样式。 它也用于 XML 和 SVG 文件。

在 XML 和 SVG 文件的情况下,:root 不是选择 html 元素,而是选择它们的根元素(例如,SVG 文件中的 svg 标签)。

因此,全局作用域自定义属性的最佳实践是使用 :root。但是,如果您正在创建网站,您可以将其放到 html 选择器上,不会有任何区别。

也就是说,由于 每个人 都在使用 :root,因此它很快就成为了一个“标准”。它还有助于将以后使用的变量与正在积极地为文档设置样式的选择器分开。

为什么全局作用域不适合所有情况

使用 Sass 和 Less 等 CSS 预处理器,我们大多数人都会将变量存放在一个专门用于它们的 partial 文件中。这非常有效,那么为什么我们突然要考虑本地化作用域的变量呢?

其中一个原因是,有些人可能会发现自己正在做这样的事情。

:root {
  --clr-light: #ededed;
  --clr-dark: #333;
  --clr-accent: #EFF;
  --ff-heading: 'Roboto', sans-serif;
  --ff-body: 'Merriweather', serif;
  --fw-heading: 700;
  --fw-body: 300;
  --fs-h1: 5rem;
  --fs-h2: 3.25rem;
  --fs-h3: 2.75rem;
  --fs-h4: 1.75rem;
  --fs-body: 1.125rem;
  --line-height: 1.55;
  --font-color: var(--clr-light);
  --navbar-bg-color: var(--clr-dark);
  --navbar-logo-color: var(--clr-accent);
  --navbar-border: thin var(--clr-accent) solid;
  --navbar-font-size: .8rem;
  --header-color: var(--clr-accent);
  --header-shadow: 2px 3px 4px rgba(200,200,0,.25);
  --pullquote-border: 5px solid var(--clr-light);
  --link-fg: var(--clr-dark);
  --link-bg: var(--clr-light);
  --link-fg-hover: var(--clr-dark);
  --link-bg-hover: var(--clr-accent);
  --transition: 250ms ease-out;
  --shadow: 2px 5px 20px rgba(0, 0, 0, .2);
  --gradient: linear-gradient(60deg, red, green, blue, yellow);
  --button-small: .75rem;
  --button-default: 1rem;
  --button-large: 1.5rem;
}

当然,这为我们提供了一个管理自定义属性样式的地方。但是,为什么我们需要在 :root 中定义 --header-color--header-shadow?这些不是全局属性,我显然是在我的标题中使用它们,而不是其他地方。

如果它不是全局属性,为什么要全局定义它?这就是本地作用域发挥作用的地方。

本地作用域属性的实际应用

假设我们有一个需要设置样式的列表,但是我们的网站正在使用图标系统,为了简单起见,我们假设是 Font Awesome。我们不想使用 ul 标记的 disc 作为项目符号,我们想要一个自定义图标!

如果我想将无序列表的项目符号替换为 Font Awesome 图标,我们可以做类似的事情

ul {
  list-style: none;
}

li::before {
  content: "\f14a"; /* checkbox */
  font-family: "Font Awesome Free 5";
  font-weight: 900;
  float: left;
  margin-left: -1.5em;
}

虽然这样做非常容易,但其中一个问题是图标变得抽象。除非我们经常使用 Font Awesome,否则我们不会知道 f14a 代表什么,更不用说将其识别为复选框图标了。它在语义上毫无意义。

我们可以使用自定义属性来帮助阐明这一点。

ul {
  --checkbox-icon: "\f14a";
  list-style: none;
}

一旦我们开始使用几个不同的图标,这就会变得更加实用。让我们提高复杂度,假设我们有三个不同的列表

<ul class="icon-list checkbox-list"> ... </ul>

<ul class="icon-list star-list"> ... </ul>

<ul class="icon-list bolt-list"> ... </ul>

然后,在我们的 CSS 中,我们可以为不同的图标创建自定义属性

.icon-list {
  --checkbox: "\f14a";
  --star: "\f005";
  --bolt: "\f0e7";

  list-style: none;
}

当我们想要实际应用图标时,本地作用域自定义属性的真正力量就显现出来了。

我们可以将 content: var(--icon) 设置到我们的列表项上

.icon-list li::before {
  content: var(--icon);
  font-family: "Font Awesome Free 5";
  font-weight: 900;
  float: left;
  margin-left: -1.5em;
}

然后,我们可以使用更具意义的命名为每个列表定义该图标

.checkbox-list {
  --icon: var(--checkbox);
}

.star-list {
  --icon: var(--star);
}

.bolt-list {
  --icon: var(--bolt);
}

我们可以通过添加颜色来进一步提高这一点

.icon-list li::before {
  content: var(--icon);
  color: var(--icon-color);
  /* Other styles */
}

将图标移到全局作用域

如果我们正在使用图标系统,例如 Font Awesome,那么我假设我们不仅仅将其用于替换无序列表中的项目符号。只要我们在多个地方使用它们,将图标移到 :root 就有意义,因为我们希望它们在全局范围内可用。

将图标放在 :root 中并不意味着我们不能再利用本地作用域自定义属性!

:root {
  --checkbox: "\f14a";
  --star: "\f005";
  --bolt: "\f0e7";
  
  --clr-success: rgb(64, 209, 91);
  --clr-error: rgb(219, 138, 52);
  --clr-warning: rgb(206, 41, 26);
}

.icon-list li::before {
  content: var(--icon);
  color: var(--icon-color);
  /* Other styles */
}

.checkbox-list {
  --icon: var(--checkbox);
  --icon-color: var(--clr-success);
}

.star-list {
  --icon: var(--star);
  --icon-color: var(--clr-warning);
}

.bolt-list {
  --icon: var(--bolt);
  --icon-color: var(--clr-error);
}

添加回退

我们可以通过将其设置为回退来设置默认图标(例如 var(--icon, "/f1cb")),或者,由于我们正在使用 content 属性,我们甚至可以添加错误消息 var(--icon, "no icon set")

查看 CodePen 上的
使用 CSS 自定义属性的自定义列表图标
,作者是 Kevin (@kevinpowell)
CodePen 上。

通过将 --icon--icon-color 变量本地化,我们极大地提高了代码的可读性。如果项目中有新人加入,他们会更容易了解代码的工作原理。

当然,这不仅限于 Font Awesome。本地化作用域的自定义属性也非常适合 SVG 图标系统。

:root {
  --checkbox: url(../assets/img/checkbox.svg);
  --star: url(../assets/img/star.svg);
  --baby: url(../assets/img/baby.svg);
}

.icon-list {
  list-style-image: var(--icon);
}

.checkbox-list { --icon: checkbox; }
.star-list { --icon: star; }
.baby-list { --icon: baby; }

使用本地作用域属性创建更模块化的代码

虽然我们刚刚看到的示例可以有效地提高代码的可读性,这很棒,但我们还可以使用本地作用域属性做更多的事情。

有些人喜欢现在的 CSS;有些人讨厌使用级联的全局作用域。我并不是在这里讨论 CSS-in-JS(已经有足够多的聪明人讨论过它了),但是 本地作用域自定义属性为我们提供了一个绝佳的折衷方案

通过利用本地作用域自定义属性,我们可以创建非常模块化的代码,从而减轻了为有意义的类名绞尽脑汁的痛苦。

让我们,界定一下场景。

人们对 CSS 感到沮丧的部分原因是,以下标记在我们要为某些东西设置样式时可能会导致问题。

<div class="card">
  <h2 class="title">This is a card</h2>
  <p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Libero, totam.</p>
  <button class="button">More info</button>
</div>

<div class="cta">
  <h2 class="title">This is a call to action</h2>
  <p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Aliquid eveniet fugiat ratione repellendus ex optio, ipsum modi praesentium, saepe, quibusdam rem quaerat! Accusamus, saepe beatae!</p>
  <button class="button">Buy now</button>
</div>

如果我为 .title 类创建样式,它将同时为包含 .card.cta 类的元素设置样式。我们可以使用复合选择器(例如 .card .title),但这会提高特异性,从而导致可维护性降低。或者,我们可以采取 BEM 方法,将我们的 .title 类重命名为 .card__title.cta__title,以便更有效地隔离这些元素。

本地作用域自定义属性为我们提供了一个很好的解决方案。我们可以将它们应用到将使用它们的元素中

.title {
  color: var(--title-clr);
  font-size: var(--title-fs);
}

.button {
  background: var(--button-bg);
  border: var(--button-border);
  color: var(--button-text);
}

然后,我们可以分别在其父选择器内控制所需的一切

.card {
  --title-clr: #345;
  --title-fs: 1.25rem;
  --button-border: 0;
  --button-bg: #333;
  --button-text: white;
}

.cta {
  --title-clr: #f30;
  --title-fs: 2.5rem;
  --button-border: 0;
  --button-bg: #333;
  --button-text: white;
}

很可能,即使按钮或标题位于不同的组件中,它们之间也有一些默认值或共性。为此,我们可以构建回退,或者按照我们通常的方式为它们设置样式。

.button {
  /* Custom variables with default values */
  border: var(--button-border, 0);    /* Default: 0 */
  background: var(--button-bg, #333); /* Default: #333 */
  color: var(--button-text, white);   /* Default: white */

  /* Common styles every button will have */
  padding: .5em 1.25em;
  text-transform: uppercase;
  letter-spacing: 1px;
}

我们甚至可以使用 calc() 为按钮添加一个比例,这有可能消除对 .btn-smbtn-lg 类型类的需求(或者可以将其构建到这些类中,具体取决于情况)。

.button {
  font-size: calc(var(--button-scale) * 1rem);
  /* Multiply `--button-scale` by `1rem` to add unit */
}

.cta {
  --button-scale: 1.5;
}

以下是实际应用的更深入的解释

查看 CodePen 上的
使用 CSS 自定义属性的自定义列表图标
,作者是 Kevin (@kevinpowell)
CodePen 上。

请注意上面示例中我使用了某些通用类,例如.title.button,这些类使用局部作用域属性(借助于回退)进行样式化。通过使用自定义属性设置这些类,我可以在父选择器内局部定义它们,从而有效地为每个类提供自己的样式,而无需使用额外的选择器。

我还设置了一些带有修饰符类的价格卡片。使用通用.pricing类,我设置了所有内容,然后使用修饰符类,我重新定义了一些属性,例如--text--background,而无需担心使用复合选择器或额外的类。

通过这种方式工作,可以使代码易于维护。如果需要,可以轻松地更改属性的颜色,甚至可以创建全新的主题或样式,例如示例中价格卡片的彩虹变体。

在最初设置所有内容时需要一些先见之明,但回报会很棒。它甚至可能看起来与您习惯的处理样式的方式相反,但下次您要创建自定义属性时,请尝试将其定义在本地,如果它不需要全局存在,您将开始看到它的实用之处。