使用自定义属性“堆栈”来驯服级联

Avatar of Miriam Suzanne
Miriam Suzanne

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

自从 1994 年 CSS 问世以来,级联和继承定义了我们在网络上的设计方式。这两者都是强大的功能,但作为作者,我们对它们如何交互几乎没有控制权。选择器特异性源代码顺序提供了一些最小的“分层”控制,没有太多细微差别 - 并且继承需要一个*不间断的血统*。 **现在,CSS 自定义属性使我们能够以新的方式管理和控制级联和继承。**

我想向您展示如何使用自定义属性“堆栈”来解决人们在级联中遇到的常见问题:从范围组件样式到更明确的意图分层。

自定义属性快速介绍

与浏览器使用供应商前缀(如 -webkit--moz-)定义新属性的方式相同,我们可以使用“空” -- 前缀定义自己的自定义属性。与 Sass 或 JavaScript 中的变量一样,我们可以使用它们来命名、存储和检索值 - 但与 CSS 中的其他属性一样,它们会随着 DOM **级联**和**继承**。

/* Define a custom property */
html {
  --brand-color: rebeccapurple;
}

为了访问这些捕获的值,我们使用 var() 函数。它有两个部分:首先是自定义属性的名称,然后是该属性未定义时的回退

button {
  /* use the --brand-color if available, or fall back to deeppink */
  background: var(--brand-color, deeppink);
}

这不是对旧浏览器的支持回退。如果浏览器不理解自定义属性,它将忽略整个 var() 声明。相反,这是一种处理未定义变量的内置方式,类似于字体堆栈在字体不可用时定义回退字体系列。如果我们不提供回退,默认值为 unset

构建变量“堆栈”

这种定义回退的能力类似于在 font-family 属性上使用的“字体堆栈”。如果第一个系列不可用,将使用第二个系列,依此类推。var() 函数只接受一个回退,但我们可以嵌套 var() 函数来创建任意大小的自定义属性回退“堆栈”

button {
  /* try Consolas, then Menlo, then Monaco, and finally monospace */
  font-family: Consolas, Menlo, Monaco, monospace;

  /* try --state, then --button-color, then --brand-color, and finally deeppink */
  background: var(--state, var(--button-color, var(--brand-color, deeppink)));
}

如果嵌套的堆栈属性语法看起来很笨重,您可以使用 Sass 等预处理器使其更紧凑。

这种单回退限制是必需的,以便支持带有逗号的回退 - 例如字体堆栈或分层背景图像

html {
  /* The fallback value is "Helvetica, Arial, sans-serif" */
  font-family: var(--my-font, Helvetica, Arial, sans-serif);
}

定义“范围”

CSS 选择器允许我们深入 HTML DOM 树,并为页面上的任何元素或特定嵌套上下文的元素设置样式。

/* all links */
a { color: slateblue; }

/* only links inside a section */
section a { color: rebeccapurple; }

/* only links inside an article */
article a { color: deeppink; }

这很有用,但它没有捕捉到“模块化”面向对象或组件驱动样式的现实。我们可能有许多文章和旁注,以各种配置嵌套在一起。我们需要一种方法来明确哪个上下文或**范围**在它们重叠时应该优先。

邻近范围

假设我们有 .light 主题和 .dark 主题。我们可以在根 <html> 元素上使用这些类来定义页面范围的默认值,但我们也可以将它们应用于嵌套在各种配置中的特定组件

每次我们应用颜色模式类之一时,backgroundcolor 属性都会重置,然后被嵌套的标题和段落继承。在我们的主要上下文中,颜色从 .light 类继承,而嵌套的标题和段落从 .dark 类继承。继承基于直接血统,因此具有定义值的最近祖先将优先。我们称之为**邻近性**。

邻近性对继承很重要,但它对选择器没有影响,选择器依赖于特异性。如果我们想在黑暗或明亮容器中设置某个东西的样式,这将成为一个问题。

在这里,我尝试定义了亮色和暗色按钮变体。亮色模式按钮应该是 rebeccapurplewhite 文本,以便它们脱颖而出,而暗色模式按钮应该是 plumblack 文本。我们正在根据亮色和暗色上下文直接选择按钮,但它不起作用

一些按钮同时存在于两种上下文中,同时具有 .light.dark 祖先。在这种情况下,我们希望最近的主题接管(继承*邻近性*行为),但我们得到的是第二个选择器覆盖第一个选择器(级联行为)。由于这两个选择器具有相同的特异性,源代码顺序决定了获胜者。

自定义属性和邻近性

我们这里需要的是一种从主题*继承*这些属性,但只将它们应用于特定子元素的方法。自定义属性使这成为可能!我们可以在亮色和暗色容器上定义值,而只在嵌套元素(如我们的按钮)上使用它们继承的值。

我们将从设置按钮以使用自定义属性开始,这些属性有一个回退“默认”值,以防这些属性未定义

button {
  background: var(--btn-color, rebeccapurple);
  color: var(--btn-contrast, white);
}

现在我们可以根据上下文设置这些值,它们将根据邻近性和继承*范围*到相应的祖先

.dark {
  --btn-color: plum;
  --btn-contrast: black;
}

.light {
  --btn-color: rebeccapurple;
  --btn-contrast: white;
}

作为额外的好处,我们总体上使用了更少的代码,以及一个统一的 button 定义

我认为这就像为按钮组件创建可用参数的 API。Sara SoueidanLea Verou 都在最近的文章中很好地涵盖了这一点。

组件所有权

有时邻近性不足以定义范围。当 JavaScript 框架生成“范围样式”时,它们正在建立特定的对象元素**所有权**。“选项卡布局”组件*拥有*选项卡本身,但不拥有每个选项卡后面的内容。这也是 BEM 约定试图在复杂的 .block__element 类名中捕获的内容。

Nicole Sullivan 创造了“甜甜圈范围”这个词 来谈论 2011 年的这个问题。虽然我相信她对这个问题有更新的想法,但基本问题没有改变。选择器和特异性对于描述如何在广泛模式之上构建详细样式非常棒,但它们不能传达清晰的所有权意识。

我们可以使用自定义属性堆栈来帮助解决这个问题。我们将从在 <html> 元素上创建“全局”属性开始,这些属性用于我们的默认颜色

html {
  --background--global: white;
  --color--global: black;
  --btn-color--global: rebeccapurple;
  --btn-contrast--global: white;
}

现在,我们可以在任何需要引用它的地方使用这个默认全局主题。我们将使用 data-theme 属性来应用我们的前景和背景颜色。我们希望全局值提供一个默认回退,但我们也希望可以选择使用特定主题进行覆盖。这就是“堆栈”发挥作用的地方

[data-theme] {
  /* If there's no component value, use the global value */
  background: var(--background--component, var(--background--global));
  color: var(--color--component, var(--color--global));
}

现在,我们可以通过将 *--component 属性设置为全局属性的相反值来定义一个反转组件

[data-theme='invert'] {
  --background--component: var(--color--global);
  --color--component: var(--background--global);
}

但我们不希望这些设置继承到所有权甜甜圈之外,因此我们将这些值的重置为 initial(未定义)在每个主题上。我们希望在较低的特异性或更早的源代码顺序中执行此操作,因此它提供每个主题可以覆盖的默认值

[data-theme] {
  --background--component: initial;
  --color--component: initial;
}

initial 关键字在自定义属性上具有特殊含义,将其恢复到保证无效 状态。这意味着与其传递给 background: initialcolor: initial 设置,自定义属性将变为 undefined,而我们回退到堆栈中的下一个值,即全局设置。

我们可以对按钮做同样的事情,然后确保将 data-theme 应用于每个组件。如果没有指定特定主题,每个组件将默认为全局主题

定义“来源”

CSS 级联是一系列过滤层,用于确定当在同一属性上定义多个值时,哪个值应该优先。我们最常与**特异性**层交互,或者基于源顺序的最终分层——但级联的第一层是样式的“来源”。**来源**描述了样式来自哪里——通常是浏览器(默认值)、用户(首选项)或作者(我们)。

默认情况下,作者样式会覆盖用户首选项,用户首选项会覆盖浏览器默认值。当任何人对样式应用`!important`时,情况就会改变,来源会反转:浏览器`!important`样式具有最高的来源,然后是重要的用户首选项,然后是我们的作者重要样式,高于所有正常层。有几个额外的来源,但我们在这里不会深入探讨。

当我们创建自定义属性“堆栈”时,我们正在构建非常类似的行为。如果我们想将现有的来源表示为自定义属性堆栈,它看起来像这样

.origins-as-custom-properties {
  color: var(--browser-important, var(--user-important, var(--author-important, var(--author, var(--user, var(--browser))))));
}

这些层已经存在,所以没有理由重新创建它们。但当我们在上面分层我们的“全局”和“组件”样式时,我们正在做类似的事情——创建一个“组件”来源层,它会覆盖我们的“全局”层。相同的方法可以用来解决 CSS 中各种分层问题,这些问题不能总是用特异性来描述

  • 覆盖 » 组件 » 主题 » 默认
  • 主题 » 设计系统或框架
  • 状态 » 类型 » 默认

让我们再看看一些按钮。我们需要一个默认按钮样式、一个禁用状态以及各种按钮“类型”,例如`danger`、`primary` 和`secondary`。我们希望`disabled`状态始终覆盖类型变体,但选择器无法捕获这种区别

但我们可以定义一个堆栈,它以我们希望它们优先级排序的顺序提供“类型”和“状态”层

button {
  background: var(--btn-state, var(--btn-type, var(--btn-default)));
}

现在,当我们设置两个变量时,状态将始终优先

我已经使用这种技术创建了一个Cascading Colors框架,它允许基于分层进行自定义主题

  • HTML 中预定义的主题属性
  • 用户颜色偏好
  • 浅色和深色模式
  • 全局主题默认值

混搭

这些方法可以推向极致,但大多数日常用例可以通过堆栈中的两个或三个值来处理,通常使用上面技术组合。

  • 一个定义层的变量堆栈
  • 继承根据邻近性和范围设置它们
  • 仔细应用`initial`值以从范围中删除嵌套元素

我们一直在 OddBird 的项目中使用这些自定义属性“堆栈”。我们仍在不断发现,但它们已经在解决仅使用选择器和特异性难以解决的问题方面很有帮助。使用自定义属性,我们不必与级联或继承作斗争。我们可以按预期捕获和利用它们,并对它们在每个实例中的应用方式有更多控制。对我来说,这对 CSS 来说是一个很大的胜利——尤其是在开发样式框架、工具和系统时。