是时候开始使用 CSS 自定义属性了

已发表: 2022-03-10
快速总结↬今天,CSS 预处理器是 Web 开发的标准。 预处理器的主要优点之一是它们使您能够使用变量。 这可以帮助您避免复制和粘贴代码,并简化开发和重构。

我们使用预处理器来存储颜色、字体偏好、布局细节——主要是我们在 CSS 中使用的所有内容。

自定义元素的详细介绍

您可能听说过 Web 组件以及它们将如何永远改变 Web 开发。 最具变革性的技术是自定义元素,这是一种定义您自己的元素的方法,具有自己的行为和属性。 阅读简介 →

但是预处理器变量有一些限制:

  • 您不能动态更改它们。
  • 他们不知道 DOM 的结构。
  • 它们无法从 JavaScript 读取或更改。

作为解决这些问题和其他问题的灵丹妙药,社区发明了 CSS 自定义属性。 从本质上讲,它们的外观和工作方式类似于 CSS 变量,它们的工作方式反映在它们的名称中。

自定义属性为 Web 开发开辟了新的视野。

跳跃后更多! 继续往下看↓

声明和使用自定义属性的语法

当您开始使用新的预处理器或框架时,通常的问题是您必须学习新的语法。

每个预处理器都需要不同的声明变量的方式。 通常,它以保留符号开头——例如,Sass 中的$和 LESS 中的@

CSS 自定义属性也采用了同样的方式并使用-​-来引入声明。 但是这里的好处是您可以学习一次这种语法并在浏览器中重复使用它!

你可能会问,“为什么不重用现有的语法?”

这是有原因的。 简而言之,它提供了一种在任何预处理器中使用自定义属性的方法。 这样,我们可以提供和使用自定义属性,而我们的预处理器不会编译它们,因此属性将直接转到输出的 CSS。 而且,您可以在本机变量中重用预处理器变量,但我稍后会描述。

(关于名称:因为他们的思想和目的非常相似,有时自定义属性被称为 CSS 变量,虽然正确的名称是 CSS 自定义属性,进一步阅读,你会明白为什么这个名称最能描述它们。)

因此,要声明一个变量而不是通常的 CSS 属性,例如colorpadding ,只需提供一个以-​-开头的自定义命名属性:

 .box{ --box-color: #4d4e53; --box-padding: 0 10px; }

属性的值可以是任何有效的 CSS 值:颜色、字符串、布局值,甚至是表达式。

以下是有效自定义属性的示例:

 :root{ --main-color: #4d4e53; --main-bg: rgb(255, 255, 255); --logo-border-color: rebeccapurple; --header-height: 68px; --content-padding: 10px 20px; --base-line-height: 1.428571429; --transition-duration: .35s; --external-link: "external link"; --margin-top: calc(2vh + 20px); /* Valid CSS custom properties can be reused later in, say, JavaScript. */ --foo: if(x > 5) this.width = 10; }

如果您不确定:root匹配什么,在 HTML 中它与html相同,但具有更高的特异性。

与其他 CSS 属性一样,自定义属性以相同的方式级联并且是动态的。 这意味着它们可以随时更改,并且浏览器会相应地处理更改。

要使用变量,您必须使用var() CSS 函数并在其中提供属性的名称:

 .box{ --box-color:#4d4e53; --box-padding: 0 10px; padding: var(--box-padding); } .box div{ color: var(--box-color); }

声明和用例

var()函数是提供默认值的便捷方式。 如果您不确定是否已定义自定义属性并希望提供一个值以用作后备,您可以这样做。 这可以通过将第二个参数传递给函数来轻松完成:

 .box{ --box-color:#4d4e53; --box-padding: 0 10px; /* 10px is used because --box-margin is not defined. */ margin: var(--box-margin, 10px); }

如您所料,您可以重用其他变量来声明新变量:

 .box{ /* The --main-padding variable is used if --box-padding is not defined. */ padding: var(--box-padding, var(--main-padding)); --box-text: 'This is my box'; /* Equal to --box-highlight-text:'This is my box with highlight'; */ --box-highlight-text: var(--box-text)' with highlight'; }

运算:+、-、*、/

当我们习惯使用预处理器和其他语言时,我们希望能够在处理变量时使用基本运算符。 为此,CSS 提供了一个calc()函数,它使浏览器在对自定义属性的值进行任何更改后重新计算表达式:

 :root{ --indent-size: 10px; --indent-xl: calc(2*var(--indent-size)); --indent-l: calc(var(--indent-size) + 2px); --indent-s: calc(var(--indent-size) - 2px); --indent-xs: calc(var(--indent-size)/2); }

如果您尝试使用无单位值,则会出现问题。 同样, calc()是您的朋友,因为没有它,它将无法工作:

 :root{ --spacer: 10; } .box{ padding: var(--spacer)px 0; /* DOESN'T work */ padding: calc(var(--spacer)*1px) 0; /* WORKS */ }

范围和继承

在讨论 CSS 自定义属性范围之前,让我们回顾一下 JavaScript 和预处理器范围,以便更好地理解它们的区别。

我们知道,例如 JavaScript 变量 ( var ),作用域仅限于函数。

letconst也有类似的情况,但它们是块范围的局部变量。

JavaScript 中的closure是一个可以访问外部(封闭)函数变量的函数——作用域链。 闭包具有三个作用域链,它可以访问以下内容:

  • 它自己的范围(即在其大括号之间定义的变量),
  • 外部函数的变量,
  • 全局变量。

(查看大图)

预处理器的情况类似。 让我们以 Sass 为例,因为它可能是当今最流行的预处理器。

使用 Sass,我们有两种类型的变量:局部变量和全局变量。

全局变量可以在任何选择器或构造之外声明(例如,作为 mixin)。 否则,变量将是本地的。

任何嵌套的代码块都可以访问封闭变量(如在 JavaScript 中)。

(查看大图)

这意味着,在 Sass 中,变量的作用域完全取决于代码的结构。

但是,CSS 自定义属性是默认继承的,并且与其他 CSS 属性一样,它们是级联的。

你也不能有一个在选择器之外声明自定义属性的全局变量——这不是有效的 CSS。 CSS 自定义属性的全局范围实际上是:root范围,因此该属性是全局可用的。

让我们利用我们的语法知识,将 Sass 示例改编为 HTML 和 CSS。 我们将使用原生 CSS 自定义属性创建一个演示。 首先,HTML:

 global <div class="enclosing"> enclosing <div class="closure"> closure </div> </div>

这是CSS:

 :root { --globalVar: 10px; } .enclosing { --enclosingVar: 20px; } .enclosing .closure { --closureVar: 30px; font-size: calc(var(--closureVar) + var(--enclosingVar) + var(--globalVar)); /* 60px for now */ } 

请参阅 CodePen 上 Serg Hospodarets (@malyw) 的 Pen css-custom-properties-time-to-start-using 1。

请参阅 CodePen 上 Serg Hospodarets (@malyw) 的 Pen css-custom-properties-time-to-start-using 1。

对自定义属性的更改会立即应用于所有实例

到目前为止,我们还没有看到这与 Sass 变量有何不同。 但是,让我们在使用后重新分配变量:

在 Sass 的情况下,这没有效果:

 .closure { $closureVar: 30px; // local variable font-size: $closureVar +$enclosingVar+ $globalVar; // 60px, $closureVar: 30px is used $closureVar: 50px; // local variable } 

请参阅 CodePen 上 Serg Hospodarets (@malyw) 的 Pen css-custom-properties-time-to-start-using 3。

请参阅 CodePen 上 Serg Hospodarets (@malyw) 的 Pen css-custom-properties-time-to-start-using 3。

但是在 CSS 中,计算的值发生了变化,因为font-size值是从更改后的–closureVar值重新计算的:

 .enclosing .closure { --closureVar: 30px; font-size: calc(var(--closureVar) + var(--enclosingVar) + var(--globalVar)); /* 80px for now, --closureVar: 50px is used */ --closureVar: 50px; } 

请参阅 CodePen 上 Serg Hospodarets (@malyw) 的 Pen css-custom-properties-time-to-start-using 2。

请参阅 CodePen 上 Serg Hospodarets (@malyw) 的 Pen css-custom-properties-time-to-start-using 2。

这是第一个巨大的区别:如果您重新分配自定义属性的值,浏览器将重新计算应用它的所有变量和calc()表达式

预处理器不知道 DOM 的结构

假设我们想要使用块的默认font-size ,除了highlighted的类存在的地方。

这是HTML:

 <div class="default"> default </div> <div class="default highlighted"> default highlighted </div>

让我们使用 CSS 自定义属性来做到这一点:

 .highlighted { --highlighted-size: 30px; } .default { --default-size: 10px; /* Use default-size, except when highlighted-size is provided. */ font-size: var(--highlighted-size, var(--default-size)); }

因为具有default类的第二个 HTML 元素带有highlighted类,所以highlighted类的属性将应用于该元素。

在这种情况下,它意味着–highlighted-size: 30px; 将被应用,这反过来将使被分配的font-size属性使用–highlighted-size

一切都很简单并且有效:

请参阅 CodePen 上 Serg Hospodarets (@malyw) 的 Pen css-custom-properties-time-to-start-using 4。

请参阅 CodePen 上 Serg Hospodarets (@malyw) 的 Pen css-custom-properties-time-to-start-using 4。

现在,让我们尝试使用 Sass 实现同样的目的:

 .highlighted { $highlighted-size: 30px; } .default { $default-size: 10px; /* Use default-size, except when highlighted-size is provided. */ @if variable-exists(highlighted-size) { font-size: $highlighted-size; } @else { font-size: $default-size; } }

结果显示默认大小适用于两者:

请参阅 CodePen 上 Serg Hospodarets (@malyw) 的 Pen css-custom-properties-time-to-start-using 5。

请参阅 CodePen 上 Serg Hospodarets (@malyw) 的 Pen css-custom-properties-time-to-start-using 5。

发生这种情况是因为所有 Sass 计算和处理都发生在编译时,当然,它对 DOM 的结构一无所知,完全依赖于代码的结构。

如您所见,自定义属性具有变量作用域的优点,并添加了 CSS 属性的通常级联,了解 DOM 的结构并遵循与其他 CSS 属性相同的规则。

第二个要点是 CSS 自定义属性知道 DOM 的结构并且是动态的

CSS-Wide 关键字和all属性

CSS 自定义属性与通常的 CSS 自定义属性遵循相同的规则。 这意味着您可以为它们分配任何常见的 CSS 关键字:

  • inherit
    此 CSS 关键字应用元素父级的值。
  • initial
    这将应用 CSS 规范中定义的初始值(一个空值,或者在某些 CSS 自定义属性的情况下没有)。
  • unset
    如果属性通常被继承(如自定义属性),则应用继承的值;如果属性通常不被继承,则应用初始值。
  • revert
    这会将属性重置为用户代理样式表建立的默认值(CSS 自定义属性的情况下为空值)。

这是一个例子:

 .common-values{ --border: inherit; --bgcolor: initial; --padding: unset; --animation: revert; }

让我们考虑另一种情况。 假设您要构建一个组件,并希望确保没有其他样式或自定义属性被无意中应用到它(在这种情况下,模块化 CSS 解决方案通常用于样式)。

但现在有另一种方法:使用all CSS 属性。 此简写重置所有 CSS 属性。

与 CSS 关键字一起,我们可以执行以下操作:

 .my-wonderful-clean-component{ all: initial; }

这会重置我们组件的所有样式。

不幸的是, all关键字不会重置自定义属性。 关于是否添加-​-前缀的讨论正在进行中,这将重置所有 CSS 自定义属性。

因此,将来可能会像这样进行完全重置:

 .my-wonderful-clean-component{ --: initial; /* reset all CSS custom properties */ all: initial; /* reset all other CSS styles */ }

CSS 自定义属性用例

自定义属性有很多用途。 我将展示其中最有趣的。

模拟不存在的 CSS 规则

这些 CSS 变量的名称是“自定义属性”,那么为什么不使用它们来模拟不存在的属性呢?

其中有很多: translateX/Y/Zbackground-repeat-x/y (仍然不兼容跨浏览器), box-shadow-color

让我们尝试使最后一个工作。 在我们的示例中,让我们在悬停时更改 box-shadow 的颜色。 我们只想遵循 DRY 规则(不要重复自己),所以我们不会在:hover部分重复box-shadow的整个值,而是改变它的颜色。 救援的自定义属性:

 .test { --box-shadow-color: yellow; box-shadow: 0 0 30px var(--box-shadow-color); } .test:hover { --box-shadow-color: orange; /* Instead of: box-shadow: 0 0 30px orange; */ } 

请参阅 CodePen 上 Serg Hospodarets (@malyw) 使用 CSS 自定义属性的 Pen Emulating "box-shadow-color" CSS 属性。

请参阅 CodePen 上 Serg Hospodarets (@malyw) 使用 CSS 自定义属性的 Pen Emulating “box-shadow-color” CSS 属性。

颜色主题

自定义属性最常见的用例之一是应用程序中的颜色主题。 创建自定义属性就是为了解决这类问题。 因此,让我们为组件提供一个简单的颜色主题(应用程序可以遵循相同的步骤)。

这是我们的按钮组件的代码:

 .btn { background-image: linear-gradient(to bottom, #3498db, #2980b9); text-shadow: 1px 1px 3px #777; box-shadow: 0px 1px 3px #777; border-radius: 28px; color: #ffffff; padding: 10px 20px 10px 20px; }

假设我们想要反转颜色主题。

第一步是将所有颜色变量扩展到 CSS 自定义属性并重写我们的组件。 因此,结果将是相同的:

 .btn { --shadow-color: #777; --gradient-from-color: #3498db; --gradient-to-color: #2980b9; --color: #ffffff; background-image: linear-gradient( to bottom, var(--gradient-from-color), var(--gradient-to-color) ); text-shadow: 1px 1px 3px var(--shadow-color); box-shadow: 0px 1px 3px var(--shadow-color); border-radius: 28px; color: var(--color); padding: 10px 20px 10px 20px; }

这有我们需要的一切。 有了它,我们可以将颜色变量覆盖为反转值并在需要时应用它们。 例如,我们可以添加全局inverted HTML 类(例如, body元素)并在应用时更改颜色:

 body.inverted .btn{ --shadow-color: #888888; --gradient-from-color: #CB6724; --gradient-to-color: #D67F46; --color: #000000; }

下面是一个演示,您可以在其中单击一个按钮来添加和删除一个全局类:

请参阅 CodePen 上 Serg Hospodarets (@malyw) 的 Pen css-custom-properties-time-to-start-using 9。

请参阅 CodePen 上 Serg Hospodarets (@malyw) 的 Pen css-custom-properties-time-to-start-using 9。

如果没有复制代码的开销,则无法在 CSS 预处理器中实现此行为。 使用预处理器,您总是需要覆盖实际值和规则,这总是会产生额外的 CSS。

使用 CSS 自定义属性,解决方案尽可能干净,并且避免了复制和粘贴,因为只重新定义了变量的值。

在 JavaScript 中使用自定义属性

以前,要将数据从 CSS 发送到 JavaScript,我们经常不得不使用一些技巧,在 CSS 输出中通过纯 JSON 写入 CSS 值,然后从 JavaScript 中读取它。

现在,我们可以轻松地与 JavaScript 中的 CSS 变量进行交互,使用众所周知的.getPropertyValue().setProperty()方法读取和写入它们,这些方法用于通常的 CSS 属性:

 /** * Gives a CSS custom property value applied at the element * element {Element} * varName {String} without '--' * * For example: * readCssVar(document.querySelector('.box'), 'color'); */ function readCssVar(element, varName){ const elementStyles = getComputedStyle(element); return elementStyles.getPropertyValue(`--${varName}`).trim(); } /** * Writes a CSS custom property value at the element * element {Element} * varName {String} without '--' * * For example: * readCssVar(document.querySelector('.box'), 'color', 'white'); */ function writeCssVar(element, varName, value){ return element.style.setProperty(`--${varName}`, value); }

假设我们有一个媒体查询值列表:

 .breakpoints-data { --phone: 480px; --tablet: 800px; }

因为我们只想在 JavaScript 中重用它们——例如,在 Window.matchMedia() 中——我们可以很容易地从 CSS 中获取它们:

 const breakpointsData = document.querySelector('.breakpoints-data'); // GET const phoneBreakpoint = getComputedStyle(breakpointsData) .getPropertyValue('--phone');

为了展示如何从 JavaScript 分配自定义属性,我创建了一个响应用户操作的交互式 3D CSS 立方体演示。

这不是很难。 我们只需要添加一个简单的背景,然后放置五个立方体面,其中包含transform属性的相关值: translateZ()translateY() 、 rotateX( rotateX()rotateY()

为了提供正确的视角,我在页面包装器中添加了以下内容:

 #world{ --translateZ:0; --rotateX:65; --rotateY:0; transform-style:preserve-3d; transform: translateZ(calc(var(--translateZ) * 1px)) rotateX(calc(var(--rotateX) * 1deg)) rotateY(calc(var(--rotateY) * 1deg)); }

唯一缺少的是交互性。 该演示应在鼠标移动时更改 X 和 Y 视角( –rotateX–rotateY ),并应在鼠标滚动时放大和缩小( –translateZ )。

这是可以解决问题的 JavaScript:

 // Events onMouseMove(e) { this.worldXAngle = (.5 - (e.clientY / window.innerHeight)) * 180; this.worldYAngle = -(.5 - (e.clientX / window.innerWidth)) * 180; this.updateView(); }; onMouseWheel(e) { /*…*/ this.worldZ += delta * 5; this.updateView(); }; // JavaScript -> CSS updateView() { this.worldEl.style.setProperty('--translateZ', this.worldZ); this.worldEl.style.setProperty('--rotateX', this.worldXAngle); this.worldEl.style.setProperty('--rotateY', this.worldYAngle); };

现在,当用户移动鼠标时,演示会更改视图。 您可以通过移动鼠标并使用鼠标滚轮放大和缩小来检查这一点:

请参阅 CodePen 上 Serg Hospodarets (@malyw) 的 Pen css-custom-properties-time-to-start-using 10。

请参阅 CodePen 上 Serg Hospodarets (@malyw) 的 Pen css-custom-properties-time-to-start-using 10。

本质上,我们只是更改了 CSS 自定义属性的值。 其他一切(旋转和放大和缩小)都由 CSS 完成。

提示:调试 CSS 自定义属性值的最简单方法之一就是在 CSS 生成的内容中显示其内容(这适用于简单的情况,例如字符串),以便浏览器自动显示当前应用的值:

 body:after { content: '--screen-category : 'var(--screen-category); }

您可以在纯 CSS 演示(没有 HTML 或 JavaScript)中检查它。 (调整窗口大小以查看浏览器自动反映更改后的 CSS 自定义属性值。)

浏览器支持

所有主流浏览器都支持 CSS 自定义属性:

(查看大图)

这意味着,您可以在本地开始使用它们。

如果您需要支持较旧的浏览器,您可以学习语法和使用示例,并考虑并行切换或使用 CSS 和预处理器变量的可能方式。

当然,我们需要能够检测 CSS 和 JavaScript 的支持,以便提供回退或增强功能。

这很容易。 对于 CSS,您可以使用带有虚拟特征查询的@supports条件:

 @supports ( (--a: 0)) { /* supported */ } @supports ( not (--a: 0)) { /* not supported */ }

在 JavaScript 中,您可以将相同的虚拟自定义属性与CSS.supports()静态方法一起使用:

 const isSupported = window.CSS && window.CSS.supports && window.CSS.supports('--a', 0); if (isSupported) { /* supported */ } else { /* not supported */ }

正如我们所见,CSS 自定义属性仍然不是在每个浏览器中都可用。 知道了这一点,您可以通过检查它们是否受支持来逐步增强您的应用程序。

例如,您可以生成两个主要的 CSS 文件:一个具有 CSS 自定义属性,另一个没有它们,其中的属性是内联的(我们将很快讨论如何做到这一点)。

默认加载第二个。 然后,如果支持自定义属性,只需检查 JavaScript 并切换到增强版本:

 <!-- HTML --> <link href="without-css-custom-properties.css" rel="stylesheet" type="text/css" media="all" />
 // JavaScript if(isSupported){ removeCss('without-css-custom-properties.css'); loadCss('css-custom-properties.css'); // + conditionally apply some application enhancements // using the custom properties }

这只是一个例子。 正如您将在下面看到的,还有更好的选择。

如何开始使用它们

根据最近的一项调查,Sass 仍然是开发社区首选的预处理器。

因此,让我们考虑开始使用 CSS 自定义属性或使用 Sass 为它们做准备的方法。

我们有几个选择。

1. 手动签入支持代码

这种手动检查代码是否支持自定义属性的方法的一个优点是它可以工作并且我们现在就可以做到(不要忘记我们已经切换到 Sass):

 $color: red; :root { --color: red; } .box { @supports ( (--a: 0)) { color: var(--color); } @supports ( not (--a: 0)) { color: $color; } }

这种方法确实有很多缺点,尤其是代码变得复杂,并且复制和粘贴变得非常难以维护。

2. 使用自动处理生成的 CSS 的插件

PostCSS 生态系统今天提供了许多插件。 假设您仅提供全局变量(即您仅在:root选择器中声明或更改 CSS 自定义属性),它们中的一些处理生成的 CSS 输出中的自定义属性(内联值)并使其工作,因此它们的值可以很容易地内联。

一个例子是 postcss-custom-properties。

这个插件提供了几个优点:它使语法工作; 它与所有 PostCSS 的基础设施兼容; 它不需要太多的配置。

然而,也有缺点。 该插件要求您使用 CSS 自定义属性,因此您没有为项目准备从 Sass 变量切换的路径。 此外,您不会对转换进行太多控制,因为它是在 Sass 编译为 CSS 之后完成的。 最后,插件没有提供太多调试信息。

3. css-vars Mixin

我开始在我的大部分项目中使用 CSS 自定义属性,并尝试了许多策略:

  • 使用 cssnext 从 Sass 切换到 PostCSS。
  • 从 Sass 变量切换到纯 CSS 自定义属性。
  • 在 Sass 中使用 CSS 变量来检测它们是否被支持。

由于那次经历,我开始寻找满足我标准的解决方案:

  • 与 Sass 一起使用应该很容易。
  • 它应该易于使用,并且语法必须尽可能接近原生 CSS 自定义属性。
  • 将 CSS 输出从内联值切换到 CSS 变量应该很容易。
  • 熟悉 CSS 自定义属性的团队成员将能够使用该解决方案。
  • 应该有一种方法可以在使用变量时获得有关边缘情况的调试信息。

因此,我创建了 css-vars,一个可以在 Github 上找到的 Sass 混合。 使用它,您可以开始使用 CSS 自定义属性语法。

使用 css-vars Mixin

要声明变量,请使用 mixin,如下所示:

 $white-color: #fff; $base-font-size: 10px; @include css-vars(( --main-color: #000, --main-bg: $white-color, --main-font-size: 1.5*$base-font-size, --padding-top: calc(2vh + 20px) ));

要使用这些变量,请使用var()函数:

 body { color: var(--main-color); background: var(--main-bg, #f00); font-size: var(--main-font-size); padding: var(--padding-top) 0 10px; }

这为您提供了一种从一个地方(来自 Sass)控制所有 CSS 输出并开始熟悉语法的方法。 另外,您可以使用 mixin 重用 Sass 变量和逻辑。

当您想要支持的所有浏览器都使用 CSS 变量时,您所要做的就是添加以下内容:

 $css-vars-use-native: true;

不是在生成的 CSS 中对齐变量属性,而是 mixin 将开始注册自定义属性,并且var()实例将转到生成的 CSS 而不进行任何转换。 这意味着您将完全切换到 CSS 自定义属性,并将拥有我们讨论的所有优势。

如果要开启有用的调试信息,添加以下内容:

 $css-vars-debug-log: true;

这会给你:

  • 变量未分配但已使用时的日志;
  • 重新分配变量时的日志;
  • 未定义变量但传递了默认值时使用的信息。

结论

现在您了解了更多关于 CSS 自定义属性的信息,包括它们的语法、优点、良好的使用示例以及如何通过 JavaScript 与它们进行交互。

您已经学习了如何检测它们是否受支持,它们与 CSS 预处理器变量有何不同,以及如何开始使用原生 CSS 变量,直到它们被跨浏览器支持。

这是开始使用 CSS 自定义属性并准备在浏览器中支持它们的正确时机。