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

发布于 大漠

今天,CSS处理器已是Web开发流程中的一个标准。预处理器的主要优点之一就是它们能够使用变量。这可以帮助你避免复制粘贴代码,也简化了开发和重构。

我们使用预处理器可以存储颜色、字体和布局等细节——一切我们想使用的CSS都可以。

但预处理器的变量也有很多限制:

  • 不能动态修改变量
  • 没有DOM结构的意识
  • 不能使用JavaScript读取或更改

做为这些问题,社区发明CSS自定义属性。从本质上讲,这些工作看上去像CSS变量,它们的工作方式反映在他们的名字上。

可以说自定义属性打开Web开发的新视野。

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

每当你开始使用一个新的预处理器或框架时有一个通常的问题,那就是你必须学习新的语法。

每个处理器需要用不同的方法来声明变量。通常都会使用一个像征性的实体符(Symbol),比如说,在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属性一样,也是动态的。这意味着他们可以随时改变,相应浏览器会做处理。

使用一个变量,你必须使用CSS的var()函数,然后在里面传一个已声明好的变量名称:

.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);
}

还存在一个问题,那就是不能计算不带单位的值。

:root{
    --gap: 10;
}

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

如果你从未接触过calc()函数的使用,建议你花点时间阅读这方面的文章

作用域和继承

在谈论CSS自定义属性作用域之前,让我们先回忆JavaScript和预处理器作用域范围,以便更好的理解它们之间的差异。

众所周知,在JavaScript中的变量var,它的作用域范围是跟function有关的。

我们知道,对于letconst有一个类似的情况,但它们都是块作用域(局部变量)。

closure在JavaScript中是一个闭包,就是闭包函数访问外部(封闭)函数作用域的变量。闭包有三个范围,它的访问方式如下:

  • 自己的作用域范围(函数花括号内的变量)
  • 外部函数的变量
  • 全局变量

预处理器里的处理方式有点类似。让我们用Sass来作为一个示例,因为它可能是当今最受欢迎的预处理器。

在Sass中,变量有两种类型:本地(local)和全局(global)。

在任何选择器或构造器(比如混合宏)声明的变量是全局变量,否则是本地变量。

任何嵌套的代码块都可以访部封闭内的变量(和JavaScript类似)。

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

然而,CSS自定义属性像其他CSS属性一样,具有继承的特性。

自定义属性不能有一个全局变量在选择器之外声明——这不是有效的CSS。CSS自定义属性的全局作用域其实就是:root作用域,于是: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 */
}

到目前为止,我们还没有看到这和Sass变量有何不同之处。让我们重新分配变量后,其用法。先来看Sass的情况,但没有效果:

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

    $closureVar: 50px; // local variable
}

但在CSS,计算的值因为改变了--closureVar的值重新计算了值,因此改变了font-size的值:

.enclosing .closure {
    --closureVar: 30px;

    font-size: calc(var(--closureVar) + var(--enclosingVar) + var(--globalVar));
    /* 80px for now, --closureVar: 50px is used */

    --closureVar: 50px;
}

这是第一个巨大的差别:如果你重新分配一个自定义属性的值,浏览器将重新计算cale()表达式所运用的变量

预处理器不知道DOM的结构

假设我们想要在块元素中除了带有highlighted类的元素中使用默认的font-size字号。

<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));
}

因为第二个HTML元素除了default类名还使用了highlighted类名,所以highlighted属性将被应用到这个元素。

在这个示例中,它的意思是--highlighted-size: 30px;将会被运用,进而将--highlighted-size将用于font-size上。

其工作原理都很简单:

现在,我们尝试在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;
    }
}

结果表明,默认的大小都用于这两个元素:

这是因为所有Sass的计算和处理都是发生在编译的时候,当然,它不知道任何关于DOM结构,完全依赖于代码的结构。

正如你所看到的,自定义属性具有封必的作用域和级联的CSS属性和DOM结构有关系。

第二个结论是:CSS自定义属性可以理解DOM的结构和动态改变

CSS关键词和all属性

  • inherit:继承父元素的属性值
  • initial:CSS规范中定义的初始值(空值或者在某些情况下的CSS定义属性)
  • unset:如果属性是继承的(如自定义属性的情况下)运用inherit值,如果属性不是继承的运用initial
  • revert:重置用户代理的样式表中的默认值(自定义属性中是个空值)

这有一个示例:

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

我们来考虑另一个例子。假设你想要构建一个组件,并确保没有其他样式或者不经意间应用了自定义属性(一个模块化的CSS解决方案通常在这种情况下使用)。

但是现在有另一种方法,使用CSS属性all 。这个表示重置所有的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-xbox-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; */
}

颜色皮肤

自定义属性最常用的一个用例就是给应用程序换肤。创建自定义属性为解决这类问题变得非常简单。让我们为组件提供一个简单的颜色皮肤。

这里是一个按钮组件代码

.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;
}

假设我们想要一个反转的颜色。

第一步是将所有颜色的变量换成自定义属性重写我们的组件。所以其结果将是相同的

.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;
}

这一切是我们所需要的。通过它,我们在需要反转的时候覆盖需要的颜色变量。例如,我们可以给HTML元素上添加inverted类名(也可以在body元素上添加)和改变相应的颜色:

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

下面的示例,你可以单击一个按钮来添加和删除一个全局的类名。

这种行为在CSS预处器程序中无法实现一个没有复制代码的开销。预处理器,你总是需要覆盖实现的值和规则,这也经常会导致额外的CSS。

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

JavaScript控制自定义属性

以前,从CSS发送数据到JavaScript,我们不得不求助于一些技巧,通过JSON编写CSS,做为输出,然后通过JavaScript来阅读它。

现在,我们可以很容易的通过JavaScript来处理CSS变量,我们所知道的就是通过.getPropertyValue().setProperty()方法阅读和重写自定义属性:

/**
* 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交互的立方体案例,通过用户的操作可以控制3D盒子的效果。

它不是很难。我们只需要添加一个简单的背景,然后把通过transform属性:translateZ()translateY()rotateX()rotateY()实现立方体盒子。

提供正确的perspective值:

#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));
}

唯一缺少的是交互性。示例应该改变XY轴的视角(--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);
};

现在,当用户移动鼠标,下面的示例会改变效果。你可以通过移动鼠标,使用鼠标滚轮放大和缩小:

从本质上讲,我们已经改变了CSS自定义属性的值,其他(旋转和缩放)是由CSS完成。

**技巧:**调试一个CSS自定义属性值的最简单方法是通过CSS的content来生成自定义属性的值,那么浏览器将自动显示当前所应用的值。

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

你可以点击这里查看效果(没有HTML或JavaScript)。调整浏览器窗口自动显示CSS自定义属性值的更改。

浏览器兼容性

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
}

这只是一个例子,下面你将看到是一种更好的选择。

如何开始使用CSS自定义属性

根据最近的一项调查得知,Sass将继续开发它的预处理器功能。

因此,让我们考虑如何在Sass这样的预处理顺中如何开始使用CSS自定义属性。

我们有一些选择。

手动检查代码是否支持

这种方法的一个优点是手动检查代码是否支持自定义属性是否能工作,我们现在能做的(别忘了,我们把注意力转到Sass):

$color: red;
:root {
    --color: red;
}

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

这个方法有很多缺点,尤其是代码变得复杂,并复制和粘贴变得相当难以维护。

使用插件自动产生CSS

到今天为止,PostCSS生态系统已经提供了数十个插件。假如你只提供全局变量(你只声明或改变CSS内自定义属性:root选择器)插件可以将自定义属性输出可用的CSS。所以它们的值会变成内联样式。

比如postcss-custom-properties插件。

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

然而也有缺点。插件需要你使用CSS自定义属性,所以没有通道可以使用你项目中的Sass变量。同样,你不能很好的控制转换,因为它是在Sass之后完成CSS编译。最后,插件没有提供太多的调试信息。

CSS-VARS 混合宏

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

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

由于这种经历,我开始寻找一个解决方案,可以满足我的标准:

  • 使用Sass应该很容易
  • 应该直接使用,语法必须尽可能接近原生CSS自定义属性
  • 内联CSS输出值切换到CSS变量应该很容易
  • 团队成员熟悉CSS可以使用CSS自定义属性的解决方案
  • 应该有一个方法对边界情况有调试信息

因此,我创建了css-vars的Sass混合宏,可以在Github上看到代码。使用它,你可以开始使用CSS自定义属性的语法。

使用css-vars混合宏

像下面这样,通地混合宏来声明变量:

$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;
}

这给了你从一个地方控制所有输出的CSS(从Sass熟悉的语法开始)。另外,你可以重用Sass变量、混合宏和逻辑。

当你想要支持的浏览器使用CSS变量,那么你所要做的就是添加:

$css-vars-use-native: true;

而不是调整变量属性产生的CSS,混合宏将开始注册自定义属性,var()将由此产生没有转换的CSS。这意味着你必须完全转向CSS自定义属性,在这里我们讨论了所有的优势。

如果你想打开有用的调试信息,添加下面的代码即可:

$css-vars-debug-log: true;

这将给你:

  • 当一个变量没有声明但使用的日志
  • 当一个变量重新分配后的日志
  • 当一个变量没有定义但使用了默认值的信息

总结

现在你知道了更多关于CSS自定义属性,包括语法,他们的优势,良好的用法示例和如何通过JavaScript与自定义变量进行交互。

你已经了解了如何检测他们是否支持,它们是如何不同于CSS预处理器变量,以及如何开始使用原生CSS变量,直到能跨浏览器支持。

这是开始使用CSS自定义属性的最佳时间和浏览器支持这些原生的CSS变量。

本文根据@Serg Hospodarets的《It’s Time To Start Using CSS Custom Properties》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:https://www.smashingmagazine.com/2017/04/start-using-css-custom-properties

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.fedev.cn/css3/start-using-css-custom-properties.htmlDunk Low Pro Sb Loden Dark Black Loden 304292-301