Article

CSS 变量用户指南

长期要求但仍未充分利用的 CS S自定义属性(用于级联变量)为协作和代码重用提供了革命性的可能性。

目前,全球 93% 的用户都支持 CSS 变量。

变量 CSS 一直是对世界高度要求使用的功能,这是我在W3C 的 CSS工作组 1997-2012年加入,开发者社区在寻求减少重复和流线型工作,针对 CSS 缺少变量的问题设计了许多解决方案:首先是自定义 PHP 脚本,然后是 Less 和 Sass 之类的预处理器。工作组知道,本机解决方案可以减少对工具的需求;我们仍然需要解决变量。CSS 变量模块的第一个工作草案于 2012 年左右发布,而 CSS 自 2017 年以来,级联变量的自定义属性(更准确地说是重命名)在浏览器支持奇偶校验中获得了关注。

但是,今天,对 CSS 变量的了解仍然很少。阅读本文之后,希望您能更好地理解声明性 CSS 变量与其他编程语言中的变量之间的区别,以及如何利用它们的功能,前进!

简介 CSS 变量

CSS 变量是正常级联甚至继承的自定义属性。它们以保留的--前缀开头,关于它们的值没有真正的规则。(任何事情,甚至空格都没有。)它们在声明时被松散地解析,但是直到在非自定义属性中使用它们之后,错误处理才完成。它们的值通过函数引用,该函数var(--name)可以在任何 CSS 属性中使用。该var()函数还支持第二个参数,以防万一未设置该变量时的备用。

浏览器支持如何?

目前,全球 93% 的用户都支持 CSS 变量。如果浏览器不支持 CSS 变量,则它也不了解该 var() 函数,也不知道其第二个参数的含义。相反,我们需要使用级联,就像我们对每个新 CSS 功能所做的那样。举个例子:

background: red;
background: var(--accent-color, orange);

根据浏览器的值--accent-color,可能有四种结果。

  • 首先,如果浏览器不支持CSS变量,它将忽略第二行并应用红色背景。
  • 其次,如果浏览器确实支持 CSS 变量并且--accent-color已设置,则该颜色将成为背景。
  • 第三,如果浏览器支持 CSS 变量--accent-color且未设置,则将使用橙色,这是var()后备。
  • 第四,如果浏览器支持 CSS 的变量和--accent-color设置,但对于财产(荒谬值例如,42deg),背景将是-等待它-透明。

最后的结果可能没有即时意义。为什么透明?毕竟,如果我们这样写:

background: red;
background: 42deg;

我们将得到红色,因为第二行以及浏览器无法理解的所有内容都将被丢弃。

42deg不包含任何变量的代码段中,浏览器将在解析时将其丢弃。但是,使用变量,浏览器直到稍后才知道该声明是否有效。到那时,它已经丢弃了其他所有级联值(因为它仅保存一个计算值),并已还原为初始值,在这种情况下为transparent

当后备值存在

旧版浏览器的后备值仅适用于简单的用例,但是 CSS 功能查询(即@supports规则)可以为其提供完全不同的 CSS。考虑以下示例,该示例在不支持 CSS 变量的浏览器中设置红色背景,在不支持 CSS 变量的浏览器中设置绿色背景:

html { background: red; }

@supports (--css: variables) {
    html { background: green; }
}

功能查询有负面的版本,也使得我们有条件地添加 CSS 只是不支持浏览器 CSS 通过写变量@supports not (--css: variables),然而在这种情况下,就只能通过浏览器阅读两个支撑特征的查询和别不支持 CSS 变量,这是一个很小的集合。

CSS 变量与预处理器变量

CSS 预处理器基本上是一次执行的程序,因为它们是静态 CSS 代码,它们会生成并向下发送。它们的行为类似于命令式编程语言变量,在执行过程中具有词法作用域和多个值。它们可以在样式表中的任何位置使用:选择器,条件,属性,值等,甚至仅生成值或选择器的一部分。

相反,CSS 变量只能在值中使用,并且只能用于整个标记。它们是反应性的,并且在页面的整个生命周期中都保持活动状态。它们在每个元素的基础上使用动态作用域,并且不能成为命令式计算的一部分,因为它们对于每个给定状态都只有一个值。当在外部设置 CSS 变量时(例如,通过 HTML 或 JavaScript),请将其用于纯数据,而不是 CSS 值(例如 length 或 percents)。

动态而不是词汇范围

预处理器中的可变作用域归结为嵌套的花括号块。但是,因为 CSS 变量是属性,所以它们的作用域(与预处理器变量不同)是基于 DOM 的。这意味着 CSS 变量是按元素而不是按作用域解析的,它们像常规属性一样继承。以下 CSS 变量示例:

body {
    --shadow-color: gray;
}

button {
    box-shadow: .1em .1em .1em var(--shadow-color);
}

button:hover {
    --shadow-color: skyblue;
}

将鼠标悬停在该按钮上时,其灰色阴影将变为天蓝色。让我们尝试将其转换为预处理器语言 Sass:

body {
    $shadow-color: gray;
}

button {
    box-shadow: .1em .1em .1em $shadow-color;
}

button:hover {
    $shadow-color: skyblue;
}

结果是语法错误:“第6行的未定义变量。” Sass不知道它<body>的内部<button>(因为它没有用 CSS 在浏览器中的 HTML 上下文执行),或者a也是一个button:hover

CSS 变量是反应性的

预处理程序变量的最重要区别是 CSS 变量是反应性的。它们在页面的整个生命周期中都保持活动状态,更新它们会更新引用它们的每个关系。从这个意义上讲,它们与 Angular,Mavo 和 Vue 之类的反应式框架中的属性相比,与常规编程或预处理器语言中的变量更相似。因为它们是属性,所以可以通过更新 CSS 属性的任何机制来更新它们:样式表,内联样式,甚至 JavaScript。

循环使CSS变量在计算值时无效,考虑以下Sass片段:

.foo {
  $i: 1;
  z-index: $i;
  $i: $i + 1;
  z-index: $i;
}

输出将是这样的:

.foo {
    z-index: 1;
    z-index: 2;
}

CSS 变量是一个完全不同的故事,这是其反应性的自然结果。让我们看一下相同的代码片段,但带有 CSS 变量:

.foo {
  --i: 1;
  z-index: var(--i);
  --i: calc(var(--i) + 1);
  z-index: var(--i);
}

一个依赖于自身的变量会创建一个无限循环,而在像 CSS 这样的声明性语言中,这是不可能的。为避免这种情况,循环在计算值时将所有涉及的变量都视为无效。这包括长度为 1(如上面的例子)和较长的循环链,其中变量 A 依赖于 B,取决于 C,依此类推,直到的 Z 个循环,其取决于 A 的这些全部将是无效的,在计算值时间,这将使它们的值等于initial,并且其结果基本上与从未设置时相同。

助于真正分离和样式

CSS 变量的反应性不仅仅是哲学上的区别。它使它们功能强大。如果使用得当,样式可以保留在 CSS 中,而计算可以保留在 JavaScript 中,而每种方法都属于该样式。为了使这个概念不太抽象,假设我们需要一个径向渐变背景,其中渐变的中心点跟随鼠标光标。过去,root 每次鼠标移动时,我们都需要在 JavaScript 中生成整个渐变并将其设置为元素的内联样式。使用 CSS 变量,JavaScript 只需设置两个 CSS 变量:--mouse-x--mouse-y。在原始 JavaScript中,可能看起来像这样:

var root = document.documentElement;

document.addEventListener("mousemove", evt => {
    let x = evt.clientX / innerWidth;
    let y = evt.clientY / innerHeight;

    root.style.setProperty("--mouse-x", x);
    root.style.setProperty("--mouse-y", y);
});

然后,设计人员可以调整其心脏内容的效果,而无需与开发人员进行沟通。例如,它可能来自于此:

html {
    min-height: 100vh;
    background: radial-gradient(
        at calc(100% * var(--mouse-x, .5)) calc(100% * var(--mouse-y, .5)),
        transparent, black 80%) gray;
}

使用完全相同的中心点的分层渐变效果完全不同:

html {
    min-height: 100vh;
    --center: calc(100% * var(--mouse-x, .5)) calc(100% * var(--mouse-y, .5));
    background: radial-gradient(circle at var(--center), yellowgreen, transparent 3%),
        conic-gradient(at var(--center), yellowgreen, green, yellowgreen);
}

假设我们希望圆锥形渐变中的色相和角度根据一天中的时间而变化。鼠标也可以使用相同的 JavaScript,我们可以添加一些 JavaScript 来设置当前小时(0–23)的 CSS 变量:

var updateHour = () => {
    var hour = new Date().getHours();
    root.style.setProperty("--hour", hour);
};
setInterval(updateHour, 60000);
updateHour();

然后,我们可以将该变量包含在 CSS 中,并在计算中使用它:

html {
    min-height: 100vh;
    --center: calc(100% * var(--mouse-x, .5)) calc(100% * var(--mouse-y, .5));
    --hue: calc(var(--hour)  * 15);
    --darkColor: hsl(var(--hue), 70%, 30%);
    --lightColor: hsl(var(--hue), 70%, 50%);
    background: conic-gradient(at var(--center), var(--darkColor) calc(var(--hour) / 24 * 1turn), var(--lightColor) 0);
}

由于通过 JavaScript 设置的所有三个变量都是纯数据,而不是 CSS 值,因此我们也可以在多个不相关的 CSS 规则中使用它们。(它们不特定于我们的背景效果。)

变量有助于封装

CSS 变量还使重用和自定义 CSS 代码成为可能,因为它们使封装成为可能。假设我们已经创建了适用于该类的平面按钮样式.flat。其(简化的)CSS 代码如下所示:

button.flat {
    border: .1em solid black;
    background: transparent;
    color: black;
}

button.flat:hover {
    background: black;
    color: white;
}

假设我们需要不同颜色的按钮来执行不同的操作,例如红色按钮来执行危险操作。我们可以支持.danger修饰符类并覆盖相关的声明:

button.flat.danger {
    border-color: red;
    color: red;
}

button.flat.danger:hover {
    background: red;
    color: white;
}

为了避免重复,让我们用一个变量替换颜色:

button {
    --color-initial: black;
    border: .1em solid var(--color, var(--color-initial));
    background: transparent;
    color: var(--color, var(--color-initial));
}

button:hover {
    background: var(--color, var(--color-initial));
    color: white;
}

现在主题化是覆盖一个属性的问题--color

button.flat.danger {
    --color: red;
}

通过覆盖--color按钮的内联样式上的属性,我们甚至可以即时创建主题。

实际上,主题化第三方 CSS 代码曾经意味着要非常熟悉其内部结构,并且更改第三方代码也需要对主题代码进行类似的广泛更改。现在,CSS 自定义属性可以用作各种 API:仅通过自定义属性即可显着改变基础样式。

回到我们的按钮示例,假设我们要添加一个过渡以使悬停效果更加平滑。我们还希望新的背景色从边框向内增长,而不是在两种背景色之间转换时获得的默认淡入度。为此,我们需要使用inset伪造背景box-shadow

button.flat {
    --color-initial: black;
    border: .1em solid var(--color, var(--color-initial));
    background: transparent;
    color: var(--color, var(--color-initial));
    transition: 1s;
}

button.flat:hover {
    box-shadow: 0 0 0 1em var(--color, var(--color-initial)) inset;
    color: white;
}

尽管我们的实现发生了相当大的变化,但所有按钮的不同颜色主题都可以正常工作。如果我们需要覆盖 CSS 进行主题化,那么主题化代码将已经损坏。

多一些字符或维护时间,是自定义属性的真正价值。它们使我们能够共享可被其他开发人员重用和自定义的代码,同时仍然允许我们完全更改其内部结构。自定义属性也是主题阴影 DOM 组件的唯一方法,因为与其他 CSS 属性不同,自定义属性超越了阴影 DOM 边界。

创建调色板

通常,样式指南从一种或几种基本强调色开始,然后从中绘制调色板的其余部分。仅对基础颜色使用变量比必须手动调整每种颜色变化更有效。不幸的是,CSS 还没有颜色修改功能。(它们来自 CSS Color 5,由您亲自编辑!)同时,我们不需要将基本色定义为一个变量,而是需要对颜色分量使用单独的变量。

在 CSS 当前可用的颜色语法中,hsl()趋向于更好地用于创建颜色变化(直到我们得到lch()为止,由于它的范围更广,并且感知均匀性也更好)。如果我们预期只需要较亮/较暗和 Alpha 变体,则可以将一个变量用于色相和饱和度:

:root {
    --base-color-hs: 335, 100%;
    --base-color: hsl(var(--base-color-hs), 50%);
    --base-color-light: hsl(var(--base-color-hs), 85%);
    --base-color-dark: hsl(var(--base-color-hs), 20%);
    --base-color-translucent: hsla(var(--base-color-hs), 50%, .5);
}

我们可以在CSS中使用这些变量,也可以随时创建新的变体。是的,仍然有些重复-基本颜色明度-但是如果我们计划创建许多alpha变体,则可以创建一个具有所有三个坐标的变量,或者一个具有明度的变量。

防止继承

当使用 CSS 变量保存数据时,需要使用它们的默认行为-继承-我们在根元素上定义变量,并可以通过重新定义它们在子树上覆盖它们。但是继承常常会妨碍您。考虑以下示例,该示例使用变量来应用具有预定义颜色,偏移和模糊半径但具有可变散布的微妙发光:

* {
    box-shadow: 0 0 .3em var(--subtle-glow) gold;
}

p {
    font: 200%/1 sans-serif;
    --subtle-glow: .05em;
}

这将导致微妙的光晕不仅应用于<p>元素,还应用于元素的任何子元素,包括<a>链接,<em>强调等。

要解决此问题,我们需要通过添加--subtle-glow: initial到第一条规则来禁用继承。由于直接应用始终优先于继承的规则,因此它将覆盖继承的值,但是由于的低特异性*,它将让位于元素上指定的任何内容。

CSS自定义属性的未来

自定义属性是 CSS 的强大功能和得到良好支持的附加功能,它们的真正潜力尚未得到充分发掘。W3C 技术架构小组和 CSS 工作小组的工作组混合人员 Houdini 正在研究将扩展的 API 渲染引擎的“魔幻”方面(即以前无法通过代码访问的那些方面),其中许多将在不久的将来可用。想象一下,例如,当我们可以对 CSS 变量进行动画处理时,就可以创建很酷的效果!表演才刚刚开始。