CSS Houdini:深入理解CSS自定义属性

发布于 大漠

这几天一直在折腾CSS自定义属性,在《图解CSS:CSS自定义属性》和《CSS 自定义属性在Web组件中的应用》都在聊CSS自定义属性。但这个CSS自定义属性和今天要聊和CSS的自定义属性还是有所不同的。他们隶属于两个不同的规范,前者是CSS Custom Properties for Cascading Variables Module Level 1,后者是CSS Properties and Values API Level 1。虽然都是W3C规范中的内容,但还是有较大差异的。而今天要和大家聊的是后者,即CSS Houdini中的CSS自定义属性。

什么是自定义属性

CSS属性和值APICSS Properties and Values API Level 1)是CSS Houdini中规范中的一部分:

允许开发人员创建自己的变量,以便以后在CSS中使用

其主要思想来源于CSS处理器(比如Sass、LESS和Stylus)中的变量特性。在《图解CSS:CSS自定义属性》一文中我们可以得知,CSS处理器中的变量是静态的,只在编译时存在;而CSS自定义属性是动态的,可以在特定的选择器块中修改它们(比如在:hover@media)。除此之外,还可以通过JavaScript读取CSS自定义属性的值和更改CSS自定义属性的值。

如何定义和使用自定义属性

CSS自定义属性的定义和CSS处理器有点类似,也有着自己的标识符:--作为前缀。其名称可以是小写(建议局部使用小写)或大写(建议全局使用大写)。而自定义属性的值可以是我们任何想要的东西,比如字符串数字颜色JavaScript代码。如下面代码所示:

:root {
    --num: 10;
    --string: string;
    --content: 'content';
    --length: 100px;
   --js-prop: if (a > b) return 30; // 这个我在CSS自定义属性中还未看到有实际使用场景
}

CSS自定义属性借助var()函数(一般充当var()函数的参数)可以将CSS自定义属性作为任何CSS属性的值(但有有效值无效值之分),另外,还可以给var()提供一个回退值(fallback),比如没有显式声明CSS自定义属性时,回退值作为var()函数的第二个值传入进去,非常有用。

.class {
    width: calc(var(--num) * 1px);
    height: var(--length);
}

.class:after {
    content: var(--content);
}

如果你担心有没有显式声明CSS自定义属性时,可以在var()中提供一个回退值:

.class {
    margin: var(--margin, 10px);
}

还有一件更重要的事情,即,可以使用CSS自定义属性作为另一个CSS自定义属性的回退值,这种方式也被称为CSS自定义属性的链式调用

.class {
    margin: var(--margin, var(--root-margin, 10px));
}

上面和大家演示的是CSS自定义属性中最基本的使用,除此之外还有其他的一些使用方式,如果你想更深入的了解的话,可以阅读《图解CSS:CSS自定义属性》一文。

CSS自定义属性和自定义变量有什么区别?

实际上,CSS自定义属性和CSS变量之间没有区别,CSS自定义属性被var()调用的时候,它就从CSS自定义属性变成了CSS变量。但CSS中的自定义属和CSS Houdini中的CSS自定义属性在声明的时候有明显的差异,在CSS Houdini中使用CSS.registerProperty来声明一个自定义属性,你还能更好的控制它。因为这样声明的CSS自定义属性,你可以给自定义属性分配CSS类型设置初始值继承

目前仅在Chrome和Firefox中支持registerProperty。如果你想正常使用registerProperty,需要启用“实验性Web平台特性(Experimental Web Platform Features)”。比如在Chrome浏览器中,可以在url中输入:chrome://flags/#enable-experimental-web-platform-features来开启该功能。

如何注册自定义属性?

要注册自定义属性,需要使用CSS.registerProperty方法,并给方法传递一个对象(自定义属性的相关配置)。代码可以像下面这样:

if ('registerProperty' in CSS ) {
    CSS.registerProperty({
        name: '--color',
        syntax: '<color>',
        inherits: true,
        initialValue: 'rgba(0, 0, 0, 1)'
    })
}

首先检测浏览器是否支持注册自定义属性。如果不支持registerProperty,我们仍然可以在样式中使用CSS变量(CSS自定义属性)。而其中的配置对象是必需的。接下来,我们花点时间来看看每个配置参数的作用和意义。

name

**name**是CSS.registerProperty必须配置的一个参数。如果未配置这个参数,程序将会报错:

试想一下,你本来是想注册一个CSS自定义属性,结果不给他命名,那么怎么称呼这个属性呢?

关于自定义属性命名的另一个要求,那就必须遵循CSS自定义命名的规则,即需要用--标识符开头。这样做的原因,我们能够在CSS处理器中使用CSS自定义属性,并且能和内置的CSS属性区分开来。如果你试图以不正确的命名规则来给CSS自定义属性命名的话:

CSS.registerProperty({
    name: "-color",
    syntax: "<color>",
    inherits: false,
    initialValue: "rgba(0,0,0,1)"
});

程序则会报错:

另外,如果同时注册了两个自定义属性,而且其名称相同时,

CSS.registerProperty({
    name: "--start-color",
    syntax: "<color>",
    inherits: false,
    initialValue: "rgba(0,0,0,1)"
});

CSS.registerProperty({
    name: "--start-color",
    syntax: "<color>",
    inherits: true,
    initialValue: "rgba(0,0,0,0)"
});

浏览器也会报错:

syntax

syntax代表CSS自定义属性的类型定义(Type Definition)。它的默认值是*,类似于TypeScript中的any类型。这意味着可以给属性分配任何值。类型定义的语法很简单,即***<TYPE_NAME>**,其中TYPE_NAME应该替换你希望分配给自定义属性的实际CSS类型。在上面看到的示例中,我们给自定义属性分配了一个<color>类型,它允许你分配任何有效的CSS颜色值。例如red#000000rgba(255, 255, 255, 0)hls(240, 20%, 50%)hlsa(35, 5%, 90%, .5)等。

这个其实和CSS规范中的属性有点类似。比如我们就拿color这个属性来举例,它也有<color>这么一项的配置(属性值):

小提示,阅读W3C规范还是有一定的技巧的,如果您感兴趣的话可以阅读《理解 CSS 属性值语法》一文。

在注册CSS自定义属性时,CSS值和单位规范中有许多受支持的语法,简单地说,TYPE_NAME的值有很多种:

TYPE_NAME 描述
<length> 任何有效的长度值,比如pxemvw
<number> 任何有效的数字值
<percentage> 任何有效的百分比值,比如%
<length-percentage> 任何有效长度或百分比值,或使用<calc()>表达式组合长度和百分比的值,比如calc(100%/3 - 2*1em - 2*1px)
<color> 任何有效的颜色值,比如red#ffffff
<image> 任何有效的图像值,比如url()引入的图像,<gradient>绘制的渐变图像等
<url> 任何有效的url,比如url(https://www.fedev.cn/pinkish.png)
<integer> 任何有效的整数值,比如09
<angle> 任何有效的角度值,比如360deg400grad1rad1turn
<time> 任何有效的时间值,比如1s20ms
<resolution> 任何有效的分辨率值,比如dpidpcmdppx
<transform-list> 任何有效的变换函数值,比如matrix()translate()roate()scale()skew()
<custom-ident> 任何有效的ident,比如easelinear

有关于CSS属性值和单位更详细的介绍,可以阅读图解CSS系列中的《CSS的值和单位》一文。

在CSS自定义属性的syntax配置还可以类似TypeScript,可以是类型组合参数列表。如果要声明类型组合的话需要使用|分割符。比如:

CSS.registerProperty({
    name: '--size',
    syntax: '<length> | <percentage>',
    inherits: false,
    initialValue: '100px'
})

在上面这个示例中,注册的--size自定义属性接受<length><percentage>两种类型,比如10px100vw100%这样的值。如果提供的值与这些类型不匹配,属性则会采用自定义属性的回退值初始值

<!-- CSS -->
.button {
    --size: '#000';     // 和自定义属性指定的值类型不匹配
    width: var(--size); // 采用自定义属性注册时设置的初始值 100px
}

.box {
    --size: 10vw;
    width: var(--size, 10vw);
}

// JavaScript

const divEles = document.querySelectorAll('div');

divEles.forEach(e => {
    console.log(window.getComputedStyle(e, null).getPropertyValue("width"))
});

//  » 100px 
//  » 33.8984px

syntax指定的类型是列表时,需要在该类型的末尾添加+符号,比如:

CSS.registerProperty({
    name: '--sizes',
    syntax: '<length>+',
    inherits: false,
    initialValue: '10px 20px'
});

在上面的示例中,我们希望属性--sizes具有一个或多个以空格分隔的长度值。

如果想让列表类型的值与值之间用逗号分隔的话,需要在syntax值的末尾加上#符号:

CSS.registerProperty({
    name: '--gradient',
    syntax: '<color>#',
    inherits: false,
    initialValue: '#f36, #890'
})

我们在使用的时候就可以像下面这样使用:

.gradient {
    background: linear-gradient(to right, var(--gradient));
}

CSS自定义属性中syntaxTYPE_NAME可用的类型是有限的。比如上面表格中所列的。接下来,花点时间来了解一些可用于CSS自定义属性的类型。

<length>

<length>类型是个长度值,用来表示CSS的大小值,比如pxemremvwvh等。我们可以像下面这样声明一个带有<length>类型的CSS自定义属性:

CSS.registerProperty({
    name: '--size',
    syntax: '<length>',
    inherits: false,
    initialValue: 0
})

如果像下面这样调用已注册的--size自定义属性的话,将会是一个无效的值:

.card {
    --size: 10;
    margin: var(--size) 
}

由于--size没有带有任何长度单位,所以和<length>类型不匹配,是个无效值。当--size是个无效值时,margin属性使用初始值,即0。为了使值有效,我们需要给值带上长度单位:

.card {
    --size: 10px;
    margin: var(--size);
}

我们可以使用calc()函数来对已注册的CSS自定义属性做一些基本运算,比如:

.card {
    --size: 20px;
    width: calc(50vw + var(--size)); // » 50vw + 20px
}

另外,我们可以使用几行简单的JavaScript代码来读取已注册的CSS自定义属性和动态改变CSS自定义属性的值:

// JS
const el = document.querySelector('.card');
const styleMap = el.computedStyleMap();
const computedProp = styleMap.get('--size');

console.log(computedProp);
// » CSSUnitValue {unit: "px", value: 10}

el.style.setProperty('--size', new CSSUnitValue(computedProp.value, 'vw'));

const propValue = el.style.getPropertyValue('--size');

console.log(propValue);
// » 10vw

console.log(CSSUnitValue.parse(propValue));
// » CSSUnitValue {unit: "vw", value: 10}

const attributeProp = el.attributeStyleMap.get('--size');

console.log(attributeProp);
// » CSSUnitValue {unit: "vw", value: 10}

console.log(CSSUnitValue.parse(attributeProp));
// » CSSUnitValue {unit: "vw", value: 10}

console.log(el.computedStyleMap().get('--size'));
// » CSSUnitValue {unit: "px", value: 83.1500015258789}

在上面的示例中,使用了CSSOM中的一些API来获取和设置CSS自定义属性的值

  • 使用.computedStyleMap.get()来解析自定义属性--size
  • 使用style.setProperty()给自定义属性分配新值,并且创建内联属性(在元素的style属性中设置CSS自定义属性)
  • 使用style.getPropertyValue()获取自定义属性的值
  • 使用attributeStyleMap.get()来返回未解析的值

computedStyleMapattributeStyleMap都可以用来获取属性集,但是computedStyleMap是只读的。解析<length>属性始终返回像素值。

有关于CSS单元的操作,比如解析转换创建值,都是CSSOM规范的一部分。有关于CSSOM现在也有两个部分,一个是我们熟悉的CSS Object Model(CSSOM);另一部分是CSS Houdini中的CSS Typed Object Modle(Typed OM)。如果你对这部分知识感兴趣的话,可以阅读@Eric Bidelman的博文《Working with the new CSS Typed Object Model》。

<percentage>

从《CSS 的值和单位》一文中我们可得知,在CSS中除了像px这样的固定单位之外,还有一些相对单位,比如%。它常用于一些流式布局和颜色计算上。而在注册CSS自定义属性时,TYPE_NAME也能支持<percentage>类型,即声明百分比语法

CSS.registerProperty({
    name: '--lightness',
    syntax: '<percentage>',
    inherits: false,
    initialValue: '0%'
})

CSS.registerProperty({
    name: '--saturation',
    syntax: '<percentage>',
    inherits: false,
    initialValue: '0%'
})

.card {
    --lightness: 30%;
    --saturation: 60%;
    color: hsl(15, var(--saturation), var(--lightness));
    background-color: hsl(225, var(--saturation), var(--lightness));
}

上面示例,我们注册了两个CSS自定义属性--lightness--saturation,如果我们用于hsl()中的话,一个是亮度,另一个是饱和度。

有一点需要注意,对于单位值,如上面看到的长度(<length>)或百分比(<percentage>),就算是初始值initialValue0时,也得带上单位。这一点可能会令你感到困惑,因为在CSS中的话是不需要这么做的。因为在0的时候没有单位是一个较好的实践,而且平时编码也是这么提倡的,比如0px === 0em === 0rem === 0vw === 0%。这也是为什么在--lightness--saturation自定义属性中初始值设置为0%。如果不带上单位,用于hsl()这样的颜色函数中时,将会被视为一个无效值。

<length-percentage>

**<length-percentage>**从类型名称上就不难发现,它是<length><percentage>类型的结合物。比如下面这个示例:

// JS
CSS.registerProperty({
    name:'--size',
    syntax: '<length-percentage>',
    inherits: false,
    initialValue: 0;
})

// CSS
.card {
    --size: 10px;
    min-height: var(--size);
    
    &:nth-child(2){
        --size: 50%;
        background-color: hsl(50, var(--size), var(--size));
    }
    
    &:nth-child(3){
        --size: 50vh;
        width: var(--size);
    }
}

在上面的示例中,在不同的.card中设置了--size自定义属性不同的值。我们可以试着将其值打印出来:

const cards = document.querySelectorAll('.card');

cards.forEach(card => {
    console.log(card.computedStyleMap().get('--size'));
})

浏览器输出的结果类似下图:

前面我们提到过,如果有多个TYPE_NAME组合在一起的话,我们可以使用|符号来连接不同的类型。就<length-percentage>,我们可以尝试着用下面这种组合方式来将上面示例中的注册自定义属性--size替换:

CSS.registerProperty({
    name:'--size',
    syntax: '<length> | <percentage>',
    inherits: false,
    initialValue: 0
})

刷新浏览器,你会发现得到的结果是一样的。

<integer>

**<integer>**类型表示没有小数的数值。我们来看看使用这种TYPE_NAME声明的自定义属性:

// JS
CSS.registerProperty({
    name: '--multiple',
    syntax: '<integer>',
    inherits: false,
    initialValue: 0
})

// CSS
.card {
    --multiple: 3;
    --size: calc(var(--multiple) * 1vw);
    min-height: var(--size);
    
    &:nth-child(2){
        --size: calc(50% / var(--multiple));
        background-color: hsl(50, var(--size), var(--size));
    }
    &:nth-child(3){
        --size: calc(var(--multiple) * 20vh);
        width: var(--size);
    }
}

和前面示例一样,尝试着将--size--multiple的相关参数打印出来:

虽然我们在自定义属性时显式的设置了TYPE_NAME<integer>,但Typed OM输出的值却是<number>

如果我们在使用--multiple自定义属性不是整数数值时,比如:

.card {
    --multiple: .3;
}

这个时候将会使用--multiple的初始值0

另外还有一点,如果我们在注册CSS自定义属性的时候,syntax是的TYPE_NAME<integer>时,要是其初始值initialValue是一个浮点数,比如0.1

CSS.registerProperty({
    name: '--multiple',
    syntax: '<integer>',
    inherits: false,
    initialValue: 0.1
})

浏览器则会报错:

<number>

**<number>**和<integer>不同,他除了可以是整数还可以是浮点数。比如上面的示例中,如果我们把<integer>换成<number>,哪怕初始值是一个浮点数,也可以正常运行:

CSS.registerProperty({
    name: '--multiple',
    syntax: '<number>',
    inherits: false,
    initialValue: 0.1
})

<color>

color是CSS中一个常用的属性,它使Web具有色彩。在注册CSS自定义属性的时候,我们可以声明一个<color>类型,用来指定自定义属性是一个颜色相关的属性。比如:

CSS.registerProperty({
    name: '--box-shadow-color',
    syntax: '<color>',
    inherits: false,
    initialValue: 'currentColor'
})

声明的--box-shadow-color自定义属性可以使用CSS中任何可用的颜色值,比如#f36rgba(23,90,56,.5)rgb(0, 23,45)hsl(45, 30%, 50%)hsla(56,90%,90%,.4)currentColortransparent以及red等。调用该自定义属性之后,浏览器将识别出该自定义属性不仅是字符串,而且是颜色值。而且浏览器还知道如何在transitionanimation对其做插值计算。

<image>

**<image>**类型是最有意思的一个类型,因为它打开了对图像做疯狂事情的大门。CSS Houdini的另一个API,即CSS Paint API可以借助canvas实现不支持从元素中读取像素的方法。因此,在自定义绘制中(Custom Paint)绘制图像的唯一方法是将其作为输入属性或参数传递图像。我们可以像下面这样注册一个<image>类型的自定义属性:

CSS.registerProperty({
    name: '--pattern',
    syntax: '<image>',
    inherits: false,
    initialValue: 'url()'
})

到目前为止,自定义属性还不支持<image>类型的语法,因此上面的代码在浏览器运行的话会报错:

<image>类型还未得到支持之前,我们可以借助CSS Paint API来绘制图像,并且在图像加载(state)阶段readypendinginvalid来做判断:

  • ready:图片加载好了,那绘制加载的图像
  • pdnding:图片正在加载,还未加载完,那绘制一个图像占位符
  • invalid:图片加载失败,绘制一个别的图像(使用CSS Paint API的canvas绘制)

比如下面这样的一个示例:

<url>

**<url>**类型类似于<image>,但有一点不同,它不限于媒体MIME类型,不过该类型还在开发中。

CSS.registerProperty({
    name: '--file',
    syntax: '<url> | none',
    inherits: false,
    initialValue: 'none'
});

<angle>

角度在CSS中一般只在transformrotate()中可以看到。但在CSS Houdini中结合 CSS Paint API,可以和canvas2d上下文一起使用。CSS中的degradgradturn可以用作<angle>语法的值:

CSS.registerProperty({
    name: '--angle',
    syntax: '<angle>',
    inherits: false,
    initialValue: '0deg'
});

<time>

时间常用于transitionanimation*-delay*-duration属性上,其接受sms。在注册自定义属性时可以使用<time>类型,将其用来控制动画的时间。比如下面这个示例:

CSS.registerProperty({
    name: '--duration',
    syntax: '<time>',
    inherits: false,
    initialValue: '0ms'
});

<resolution>

CSS中用于分辨率的常见单位有dpi(每英寸有多少个点)、dpcm(每厘米有多少个点)和dppx(每像素有多少个点)。一般用于用户根据设备屏幕来调整整个画布的分辨率。同样可以注册一个用于屏幕分辨率的属性:

CSS.registerProperty({
    name: '--resolution',
    syntax: '<resolution>',
    inherits: false,
    initialValue: '1dppx'
});

<transform-function>

<transform-function>在CSS中指的是变换函数,常见的有translate()rotate()scale()skew()等。我们可以组合一些自定义属性来得到想要的transform。比如下面的示例:

CSS.registerProperty({
    name: '--rotation',
    syntax: '<transform-function>',
    inherits: false,
    initialValue: 'rotate(0deg)'
});

CSS.registerProperty({
    name: '--scale',
    syntax: '<transform-function>',
    inherits: false,
    initialValue: 'scale(1)'
});

// CSS
.ele {
    transform: var(--rotation) var(--scale);
    animation: trans 1s;
}

<transform-list>

<transform-list><transform-function>的升级版。就是由空格分隔的<transform-function>组合:

CSS.registerProperty({
    name: '--transformations',
    syntax: '<transform-list>',
    inherits: false,
    initialValue: 'rotate(90deg) translateX(5rem)'
});

前面提到过,如果syntax是一个列表,那么可以使用+来表示。那么上面的示例可以更换成下面这样:

CSS.registerProperty({
    name: '--transformations',
    syntax: '<transform-function>+',
    inherits: false,
    initialValue: 'rotate(90deg) translateX(5rem)'
});

<custom-ident>

<custom-ident>是一种特殊的TYPE_NAME类型。它使我们能够使用自己喜欢的关键词作为自定义属性的值。比如:

CSS.registerProperty({
    name: '--keyword',
    syntax: 'left-side | right-side',
    inherits: false,
    initialValue: 'left-side'
});

注意:<custom-ident>类型的自定义属性也是区分大小写的。

initialValue

initialValue对应的是CSS语法规则中的initial,即初始值。在注册CSS自定义属性时,如果syntax指定的值是非*的任何值时,就需要显式的设置initialValue的值。如果不显式的配置该值,浏览器在执行时将会报错。

CSS.registerProperty({
    name:'--color',
    syntax: '<color>', // 一个非*的TYPE_NAME
    inherits: false,
    // initialValue: 'rgba(0,0,0,1)'
})

上面的示例中,syntax的值是<color>(一个非*的值),并且代码中注释掉了initialValue,此时浏览器会报出如下图所示的错误信息:

就算是显式的配置了initialValue的值,但和syntaxTYPE_NAME类型的值不匹配的话,浏览器同样也会报错。比如上面的示例,把initialValue前的注释去掉,并且将初始值更换为10px

CSS.registerProperty({
    name:'--color',
    syntax: '<color>', // 一个非*的TYPE_NAME
    inherits: false,
    initialValue: '10px'
})

通过对前面知识的了解,我们知道<color>类型的值有:

  • 基本颜色的关键词,比如blacksilvergray
  • 数值颜色值,比如rgb()rgba()hsl()hsla()
  • 扩展颜色的关键词,比如aquabeigetomato
  • 其他关键词,比如transparentcurrentColor

而上面示例syntax<color>TYPE_NAME),其初始值(initialValue)为10px,不符合<color>类型的值和单位规范要求,浏览则会出如下图的错误信息:

如果syntax的值是*,那么initialValue的值可配也可以不配,而且也可以是任何的值:

CSS.registerProperty({
    name:'--any-prop',
    syntax: '*',
    inherits: false,
    // initialValue: '10px'
})

另外,就算不是使用CSS.registerProperty注册的CSS自定义属性(CSS变量),它同样有initialValue值,只不过该值是空的。也就是说,如果没有初始化值,将会使用回退值:

body {
    background-color: var(--theme-color, #f36);
}

上面示例中,bodybackground-color的值将会是#f36,因为--theme-color没有初始化,其初始值为空。如果我们注册了--theme-color呢,并且给--theme-colorinitialValue设置一个初始值,比如#90f

CSS.registerProperty({
    name: '--theme-color',
    syntax: '<color>',
    inherits: false,
    initialValue: '#90f'
})

上面示例,使用CSS.registerProperty注册了--theme-color之后,如果在body中继续:

body {
    background-color: var(--theme-color, #f36);
}

这个时候background-color就不是#f36了,而是#90f。这是因为,我们在注册--theme-color自定义属性的时候,显式的为该属性配置了一个初始值#90f

如果我们不想使用--theme-color的初始值,就需要在使用--theme-color的时候重新设置一个值:

body {
    --theme-color: #f890fe;
    background-color: var(--theme-color, #f36);
}

除此之外,还有一个重要点需要注意,就是CSS中的initial关键词。比如说--theme-color设置了initial值之后又会发生什么呢?来看一个示例:

CSS.registerProperty({
    name: '--theme-color',
    syntax: '<color>',
    inherits: true,
    initialValue: '#90f'
})

<!-- HTML -->
<div class="parent">
    This is parent (whitout initial)
    <div class="child">
        This is child
    </div>
</div>

// CSS
.parent {
    --theme-color: #f0f;
    color: var(--theme-color);
        
    .child {
        color: var(--theme-color);
    }
    
    &.initial .child {
        --theme-color: initial;
    }
}

上面的示例中,.initial .child继承了--theme-color属性,然后将其值设置为initial。此时,我们可以看到.initial .childborder-colorcolor都是#90f(即注册--theme-color时的初始值#90f):

如果你对CSS的initial不太了解,建议你花点时间阅读《图解CSS:CSS层叠和继承》一文。

inherits

**inherits**是一个布尔值,用来配置注册的自定义属性是否具备继承的特性。在最新规范更新之后,其默认值是false。设置默认值的主要原因是从性能上来考虑的,因为浏览器不需要遍历DOM树来定义哪些节点继承了属性。如果想要继承属性,就需要将inherits设置为true

// JS
CSS.registerProperty({
    name: '--color',
    syntax: '<color>',
    inherits: true,
    initialValue: 'rgba(0, 0, 0, 1)'
})

<!-- HTML -->
<div class="parent">
    This is Parent
    <div class="child">This is Child</div>
</div>

// CSS
.parent {
    --color: #f36;

    .child {
        color: var(--color, blue);
    }
}

上面示例中,div.childdiv.parent的子元素。我们注册了一个--color的自定义属性(其初始值是rgba(0,0,0,1)),同时在.parent显式的设置了--color的值为#f36(父容器),在其子元素.child中设置了color:var(--color, blue)(设置了一个回退值blue)。最终浏览器渲染出来的效果如下:

从效果上可以看出来,子元素div.child的文本颜色是#f36,该属性从DOM中的div.parent中继承过来的。

CSS 自定义属性的未来

经过前面的学习,我们知道如何来注册一个CSS自定义属性,以及怎么在CSS中使用,又是如何通过CSSOM来操作已注册的CSS自定义属性。虽然有一些功能还不足够完善,甚至支持该特性的浏览器并不太多。但这些特性对于CSSer来说是强大的,足以吸引我们去努力学习和推进的。

虽然CSS自定义属性足够强大了,但在CSS Houdini社区的前辈并未停止该特性的继续发展。在CSS属性和值 Level 2(CSS Properties And Values API Level 2)规范有一个很好的建议:

在CSS样式表中注册自定义属性

即,在应该使用CSS自定义属性的相同位置声明自定义属性。这样做是有其一定的道理存在的:

  • 更符合逻辑(哪需要,我在哪声明)
  • 不需要再次验证变量

在未来,你看到注册CSS自定义属性,可能像下面这样:

@property --highlight-color {
    syntax: '<color>';
    initial-value: red;
    inherits: true;
}

使用CSS的@规则(@property),其和现在在JavaScript通过CSS.registerProperty()注册一个CSS自定义属性是等效的:

CSS.registerProperty({
    name: '--highlight-color',
    syntax: '<color>',
    initialValue: 'red',
    inherits: true
});

现在这种@property定义还没有最终成文,还在讨论当中。

小结

自定义属性和值,其功能是强大的,这也是CSS中的一个重大改变。有很多都是我们不敢想象的,因为有了该特性之后,我们的想象空间更大了。而且赋予CSS的能力也更强了。正应了那句,没有实现不了的,只有想不到的

虽然现在CSS Houdini相关的功能模块还没有完全得到主流游览器的支持,但这些功能模块是非常强大的。我坚信终有一天,这些特性都会来到我们的世界当中。当然,这也需要整个社区一起努力,他们才会来得更快。

扩展阅读