CSS Houdini: @property注册自定义属性

发布于 大漠

@property是CSS中新增的一个@规则的属性,是**CSS Houdini** 的 CSS属性和值 API Level 1 (CSS Properties and Values API Level 1)一个属性,可以用来自定义CSS属性,也被称为CSS的变量。为了能很好的区分CSS自定义属性,我更喜欢将其称为CSS Houdini的自定义属性(或变量)。你可能会好奇,它和原生的CSS自定义属性有何不同,又有何独特的特性,可以用来做什么?如果你感兴趣的话,请继续往下阅读。

CSS Houdini是什么?

CSS Houdini是一组底层API,它们公开了CSS引擎的各个部分,从而使开发人员能够通过加入浏览器渲染引擎的样式和布局过程来扩展CSS。CSS Houdini是一组API,它们使用开发人员可以直接访问 CSS对象模型(CSSOM),使开发人员可以编写浏览器可以解析为CSS的代码,从而创建新的CSS功能,而无需等待它们在浏览器中本地实现。

从技术上讲,可以通过访问浏览器执行的每个阶段来将文本文件(Text Files)渲染为屏幕上的像素。我们可以把每个阶段都分解成这样:

首先是解析,浏览器读取和解码HTML和CSS文件,然后浏览器会以DOM和CSSOM的形式构建这些文件的对象,由此派生出渲染树(Render Tree)或层树(Layer Tree),这是一种应用于每个元素的样式列表,接着浏览器通过布局(Layout)、**绘制(Paint)合成(Composite)**三个步骤绘制每个元素。

现在,如果我们想要构建一个花哨的图形效果,我们必须改变DOM。这是浏览器核心机制的唯一可用阶段。

上图是浏览器的渲染管道,仅DOM可用

而CSS Houdini启用所有的内部步骤,如下图所示:

为了实现这一点,许多新的API(主要是JavaScript)都在往标准化方面积极推进。

CSS Houdini的优点

当样式改变时,CSS Houdini相比JavaScript的ElementCSSInlineStyle.style的方式能够更快的解析。浏览器在应用脚本中发现的任何样式更新之前,会对CSSOM进行解析,包括上面提到的 布局绘制合成 三个过程。此外,对于JavaScript样式更新,布局、绘制和合成过程也会重复进行。CSS Houdini代码不会等待第一个渲染周期完成。相反,它被包含在第一个周期中,即,创建可渲染的,可理解的样式。CSS Houdini为在JavaScript中使用CSS值提供了一个基于对象的API。

CSS Houdini的CSSOM是一个包含类型和方法的CSS对象,并且暴露出了作为JavaScript对象的值。比起先前基于字符串的,对HTMLElement.style进行操作的方案,对JavaScript对象进行操作更符合直觉。每个元素和样式表规则都拥有一个样式对应表,该对应表可以通过StylePropertyMap来获得。

一个CSS Houdini的特性其实就是 Worklet。在它的帮助下,你可以通过引入一行JavaScript代码来引入配置化的组件,从而创建模块式的CSS。不依赖任何前置处理器、后置处理器或者JavaScript框架:

<script> 
    CSS.paintWorklet.addModule('csscomponent.js'); 
</script>

以上添加进的模块包含一个registerPaint()函数,这个模块是完全通过可配置的 worklets 来注册的。

这个 CSS paint() 函数的参数包括 worklet 的名字,以及其他可选的参数。worklet 同时能够访问元素的自定义属性:它们不需要作为函数参数传递。

li {
    background-image: paint(myComponent, stroke, 10px);
    --hilights: blue;
    --lowlights: green;
}

CSS Houdini API

CSS Houdini包括七种类型的API

  • CSS Parser API:这是一个更直接地暴露出 CSS 解析器的 API,能够把任意 CSS 类语言解析成为一种中间类型
  • CSS Properties and Values API:定义了一个用来注册新的 CSS 属性的 API。通过该 API 注册的属性必须用一种特定的解析语法书写,以定义其类型、继承行为以及初始值
  • CSS Typed OM(CSSOM):该 API 将 CSSOM 字符串转化为有类型意义的 JavaScript。这将对后续的一个重要的表现打下基础。CSSOM 值以类型化处理的 JavaScript 对象的形式暴露出来,以使其表现可以被控制
  • CSS Layout API:作为一个被设计来提升 CSS 扩展性的 API,该 API 能够让开发者去书写他们自己的布局算法,比如 masonry 或者 line snapping
  • CSS Painting API:作为一个被设计来提升 CSS 扩展性的 API,该 API 允许开发者通过 paint() 方法书写 JavaScript 函数,以控制绘制元素的背景、边框或者内容区域
  • Worklets: 该 API 允许脚本独立于 JavaScript 执行环境,运行在渲染流程的各个阶段。Worklets 在概念上很接近于 Web Workers ,它由渲染引擎调用,并扩展了渲染引擎
  • CSS Animation API:该API让我们能够基于用户输入驱动关键动画效果

这些规范目前都还处于W3C的ED阶段:

有关于CSS Houdini更多的资料还可以阅读:

CSS Houdini的自定义属性

CSS原生的自定义属性(也常称为变量)受到很多开发者的青眯。在CSS中的使用频率越来越高。它的使用非常简单:

:root {
    --primary-color: green;
}

.element {
    color: var(--primary-color);
}

除了在:root{}中声明CSS自定义属性之外,还可以在其他的选择器中声明CSS自定义属性:

.element {
    --primary-color: green;
    color: var(--primary-color)
}

简单地说,CSS自定义属性可以在任何{}代码块中以--声明,并且通过var()函数来调用。

有关于CSS自定义属性更详细的介绍可以阅读《图解CSS:CSS自定义属性》一文。

虽然CSS自定义属性提供了一种定义用户控制属性的方法,但是除了引用var()进行重用之外,它们的作用有限,不能进行更严格的定义。不过,CSS Houdini中的CSS Properties and Values API Level 1对原生CSS自定义属性进行了扩展,该规范中也提供了自定义属性(我更喜欢称之为 CSS Houdini 自定义属性),允许我们注册属性并定义它们的类型初始值确定继承。这提供了很大的功能和灵活性。

CSS Houdini的自定义属性声明方式有两种,一种是在JavaScript中使用CSS.registerProperty()来声明:

CSS.registerProperty({
    name: '--color',        // 一个DOMString,指定被定义的属性的名称
    syntax: '<color>',      // 表示所定义属性的预期语法的DOMString。默认为“*”
    inherits: false,        // 一个布尔值,定义自定义的属性是否应该被继承(真),或不(假)。默认值为false。
    initialValue: '#c0ffee',// 表示自定义属性的初始值的DOMString。
});

上面的代码表示:

  • 在JavaScript脚本中使用CSS.registerProperty()来自定义一个属性
  • name的值表示自定义属性的名称,比如--color
  • syntax的值表示自定义属性的语法规则,也就是--color是一个<color>的语法规则
  • inherits的值表示自定义属性是否能被继承
  • initialValue的值用来指定自定义属性的初始值,比如#c0ffee

我们来看一个简单地示例:

// JavaScript
CSS.registerProperty({
    name: '--box-shadow-blur',
    syntax: '<length>',
    inherits: false,
    initialValue: '0px'
})

/* CSS */
.el {
    --box-shadow-blur: 3px;
    box-shadow: 0 3px var(--box-shadow-blur) #000;
    transition: --box-shadow-blur .45s;
}
.el:hover {
    --box-shadow-blur: 10px;
}

效果如下:

有关于CSS.registerProperty()注册一个自定义属性更详细的介绍还可以阅读《CSS Houdini:深入理解CSS自定义属性》一文。

除了CSS.registerProperty()方法来注册一个CSS自定义属性之外,还可以使用@property来声明CSS自定义属性。

@property --color {
    syntax: '<color>';
    initial-value: #c0ffee;
    inherits: false;
}

@property规则需要syntaxinherits,这两者缺一不可,如果缺少任何一个,则整个规则无效。只有当syntax是通用语法定义时,初始值(initial-value)描述符才是可选的,否则描述符是必须的,如果缺少它,则整个规则无效。

仔细看看@property中的syntax。有许多有效的选项,从数字到颜色到<custom-ident>类型。还可以使用以下值修改这些语法:

  • 附加+,表示它接受该语法以一个空格分隔的值列表。例如,<length>+将是一个空格分隔的长度列表
  • 附加#,表示它接受该语法以逗号分隔的值列表。例如,<color>#将是一个逗号分隔的颜色列表
  • 在语法或标识符之间添加|,表示提供的任何选项都是有效的。例如<color># | <url> | magic允许以逗号分隔的颜色列表,url或单词magic

我们可以使用@property来重新实现上面CSS.registerProperty()实现的示例:

@property --box-shadow-blur {
    syntax: "<length>";
    inherits: false;
    initial-value: 0;
}

.el {
    --box-shadow-blur: 3px;
    box-shadow: 0 3px var(--box-shadow-blur) #000;
    transition: --box-shadow-blur 0.45s;
}
.el:hover {
    --box-shadow-blur: 10px;
}

效果如下:

从上面示例中你可能已经发现了,CSS.registerProperty()@property都是用来自定义CSS属性的,只不过前者是可以通过JavaScript来实现动态注册CSS自定义属性,后者可以直接在CSS样式表中注册自定义属性。也可以说他们仅仅是在注册CSS自定义属性的方式上不同,他们的功能和最终在CSS中的使用都是一样的。另外还有一点不一样的是,CSS.registerProperty()是从Chrome 78+开始支持,而@property是从Chrome 85+开始支持。

CSS Houdini 自定义属性 vs. CSS原生自定义属性

你可能会感到好奇,既然我们可以直接使用CSS自定义属性了,为什么CSS Houdini还需要对自定义属性做扩展呢?其实他们之间有着明显的差异。

CSS原生自定义属性的值都是字符串。比如:

:root {
    --stop: 50%;
}

const customName = document.documentElement.style.getPropertyPriority(
"--stop"
);

console.log(typeof customName); // => string

但我们使用@propertyCSS.registerProperty()注册的CSS Houdini自定义属性可以指定值类型:

@property --stop {
    syntax: "<percentage>";
    initial-value: 50%;
    inherits: false;
}

这个时候浏览器就知道--stop自定义属性是一个百分比值(<percentage>)而不是一个字符串。我们来看一个示例:

/* 原生CSS定义属性 */
:root {
    --stopPoint: 50px;
}

/* CSS Houdini 注册的CSS自定义属性 */
@property --stop {
    syntax: "<percentage>"; /* 指定值的语法类型是百分比 */
    initial-value: 50%;
    inherits: false;
}

我们在body上调用--stop自定义属性,并且重新声明它的值为50px

body {
    --stop: 50px;
    background-image: linear-gradient(
        to right,
        red,
        red var(--stop),
        gold var(--stop),
        gold
    );
}

虽然在body中显式的重置了--stop: 50px,但最终还是运用了@property中声明--stop的初始值50%,那是因为在body中重置的--stop值并不是<percentage>值,而是<length>值。

接着在div中调用:root{}中声明的--stopPoint,并且重新声明它的值为30%:

div {
    --stopPoint: 30%;
    background-image: linear-gradient(
        to right,
        gold,
        gold var(--stopPoint),
        red var(--stopPoint),
        red
    );
}

虽然在:root{}中声明了--stopPoint: 50px,但在div中的--stopPont: 30%覆盖了:root{}中声明的--stopPont

对比效果如下:

与任何其他自定义属性一样,你可以使用var()获取自定义属性的值,但在CSS Houdini自定义属性中,如果你在重写它时设置一个falsey值,CSS渲染引擎将发送初始值(它的回退值)。比如下面这个示例,使用@property注册了一个--colorPrimary自定义属性:

@property --colorPrimary {
    syntax: '<color>';
    initial-value: magenta;
    inherits: false;
}

.card {
    background-color: var(--colorPrimary); /* magenta */
}

.card__highlight {
    --colorPrimary: #09f;
    background-color: var(--colorPrimary); /* #09f */
}

.card__another {
    --colorPrimary: 23;
    background-color: var(--colorPrimary); /* magenta */
}

--colorPrimary的初始值是magenta,但在.card__another中开发者使用了一个无效的值23。如果没有@property,CSS解析器将忽略这个无效值。现在,解析器取的是magenta初始值,这也就是说,可以在CSS中使用真正的回退值。

在原生CSS自定义属性中,也可以通过var()来设置一个回退值,比如:

:root {
    --primaryColor: magenta
}

.card__another {
    --colorPrimary: 23;
    background-color: var(--colorPrimary, magenta); /* 无效值 */
}

.card {
    color: var(--color, magenta); /* magenta*/
}

我们换过一个示例,先来看其HTML:

<section>
    <h1>CSS Houdini</h1>
    <div></div>
</section>

同样使用@property来注册自定义属性--colorPrimary,并且inherits的值为true

@property --colorPrimary {
    syntax: "<color>";
    initial-value: magenta;
    inherits: true;
}

section {
    --colorPrimary: #09f;
    border: 5px solid var(--colorPrimary);
}

/* 继承了section中声明的--colorPrimary */
h1 {
    color: var(--colorPrimary, #f36)
}

div {
    --colorPrimary: #23; /* 这是一个无效值 */
    background-color: var(--colorPrimary); /* 继承了section中声明的--colorPrimary */
}

如果把inherits的值设置为false

@property --colorPrimary {
    syntax: "<color>";
    initial-value: magenta;
    inherits: false;
}

section {
    --colorPrimary: #09f;
    border: 5px solid var(--colorPrimary);
}

h1 {
    color: var(--colorPrimary, #f36); /* magenta */
}

div {
    --colorPrimary: #23; /* 这是一个无效值 */
    background-color: var(--colorPrimary); /* magenta */
}

效果对比如下:

另外,在CSS Houdini中使用@property注册自定义属性时,如果同时注册两个相同名称的自定义属性,那么后面的将会覆盖前面的:

@property --colorPrimary {
    syntax: "<color>";
    initial-value: magenta;
    inherits: true;
}

@property --colorPrimary {
    syntax: "<color>";
    initial-value: #09f;
    inherits: false;
}

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

div {
    background-color: var(--colorPrimary)
}

如果使用CSS.registerProperty()同时注册相同的自定义属性时则会报错:

CSS.registerProperty({
    name: "--stop",
    syntax: "<length>",
    initialValue: "100px",
    inherits: false
});

CSS.registerProperty({
    name: "--stop",
    syntax: "<percentage>",
    initialValue: "50%",
    inherits: false
});

CSS Houdini 自定义属性可以做什么?

上面我们花了一些篇幅介绍了CSS Houdini 自定义属性的基本使用,那么他能给我们真正带来什么样的变化呢?接下来,我们通过一些实例来向大家展示CSS Houdini自定义属性给我们带来的变化。

给渐变添加动画效果

在《图解CSS: CSS渐变》一文中我们多次提到过,要实现给渐变添加动画效果(不管是transiton还是animation)都是比较困难的。换句话说,它的效果会比较生硬,比如下面这个效果:

div {
    color: #fff;
    background-image: linear-gradient(to right, #0f9, #09f);
    transition: all .28s linear;
}

div:hover {
    color: #f36;
    background-image: linear-gradient(to right, #09f, #0f9);
}

即使尝试原生CSS自定义属性来给自定义属性添加transition过渡效果也并没有太大的改善:

我们来看CSS Houdini自定义属性的效果:

@property --startColor {
    syntax: "<color>";
    initial-value: magenta;
    inherits: false;
}

@property --stopColor {
    syntax: "<color>";
    initial-value: magenta;
    inherits: false;
}

@property --stop {
    syntax: "<percentage>";
    initial-value: 50%;
    inherits: false;
}

.gradient__css__houdini {
    --startColor: #2196f3;
    --stopColor: #ff9800;
    transition: --stop 0.5s, --startColor 0.2s, --stopColor 0.2s;
    background: linear-gradient(
        to right,
        var(--startColor) var(--stop),
        var(--stopColor)
    );
}

.gradient__css__houdini:hover {
    --startColor: #ff9800;
    --stopColor: #2196f3;
    --stop: 80%;
}

效果如下:

现来看@una用CSS Houdini自定义属性写的一个渐变边框效果:

@JoshWComeau在他的教程《Magical Rainbow Gradients》中也用CSS Houdini自定义属性和React Hooks结合起来构建一个神奇的彩虹渐变组件MagicRainbowButton

有关于MagicRainbowButton的代码可以从Github中获取

动态计数器

在Web开发中,动态计算数器的效果也是一种常见的效果:

以往实现上图的效果都是需要依赖JavaScript脚本或相关的JavaScript库来完成。

虽然说使用CSS的计数器属性counter-resetcounter-incrementcounter()以及@keyframes能实现一些简单的动态计数器效果:

div::after {
    content: counter(count);
    animation: counter 5s linear infinite alternate;
    counter-reset: count 0;
}

@keyframes counter {
    0% {
        counter-increment: count 0;
    }
    10% {
        counter-increment: count 1;
    }
    20% {
        counter-increment: count 2;
    }
    30% {
        counter-increment: count 3;
    }
    40% {
        counter-increment: count 4;
    }
    50% {
        counter-increment: count 5;
    }
    60% {
        counter-increment: count 6;
    }
    70% {
        counter-increment: count 7;
    }
    80% {
        counter-increment: count 8;
    }
    90% {
        counter-increment: count 9;
    }
    100% {
        counter-increment: count 10;
    }
}

上面的方法虽然能模拟出计数器动画效果,但对于数字大的场景,@keyframes就有点难度了。但是CSS Houdini自定义属性会给我们实现该效果带来更大的便利:

@property --num {
    syntax: "<integer>";
    initial-value: 0;
    inherits: false;
}

div {
    counter-reset: num var(--num);
    animation: counter 50s infinite alternate ease-in-out;
}

div::after {
    content: counter(num);
}

@keyframes counter {
    from {
        --num: 0;
    }
    to {
        --num: 100;
    }
}

效果如下:

动态调整颜色

从《图解CSS: CSS 颜色》一文中可以知道,在CSS中定义颜色的方式有很多种,比如hsl()hsla()就是其中之一。可以调整H(hub)的值来改变颜色。在CSS Houdini中,我们可以对H(色相)注册成一个自定义CSS属性,这样一来就可以在transitionanimation中动态调整颜色,实现颜色调整的Web动效。比如下面这个示例:

@property --hue {
    syntax: "<integer>";
    inherits: true;
    initial-value: 0;
}

:root {
    --bg: #1a1a1a;
    --button-bg: #000;
}

.button {
    --border: hsl(var(--hue, 0), 0%, 50%);
    --shadow: hsl(var(--hue, 0), 0%, 80%);

    background: var(--button-bg);
    border-color: var(--border);
    box-shadow: 0 1rem 2rem -1.5rem var(--shadow);
    transition: transform 0.2s, box-shadow 0.2s;
}

.button:hover {
    --border: hsl(var(--hue, 0), 80%, 50%);
    --shadow: hsl(var(--hue, 0), 80%, 50%);
    animation: hueJump 0.75s infinite linear;
    transform: rotateY(10deg) rotateX(10deg);
}

.button:active {
    transform: rotateY(10deg) rotateX(10deg) translate3d(0, 0, -15px);
    box-shadow: 0 0rem 0rem 0rem var(--shadow);
    animation-play-state: paused;
}

@keyframes hueJump {
    to {
        --hue: 360;
    }
}

效果如下:

@Jhey使用该特性构建了一个更复杂的Demo:

制作Web动效

CSS Houdini自定义属性在制作Web动效中也非常有优势。比如,@dancwilson就使用CSS.registerProperty()创建了一个Web动效。在这个例子中,每个元素仍然有一个关键帧动画(@keyframes),但在body:hover中,居中的元素将在Z轴上做一个额外的旋转,在动画继续进行的同埋平稳地执行它的变换。由于CSS.registerProperty()定义了自定义属性,浏览器已经拥有了插入值所需的所有信息。

<!-- HTML -->
<div>
    <p>
        <span></span>
    </p>
</div>

// JavaScript
if (window.CSS && CSS.registerProperty) {
    document.documentElement.classList.add('supported');

    ['x1','x2','y1','y2','z1','z2'].forEach(prop => {
        CSS.registerProperty({
            name: `--r${prop}`,
            syntax: '<angle>',
            inherits: false,
            initialValue: '0deg'
        });
    });
}

/* CSS */
div, p, span {
    animation: custom-prop-o-rama 8300ms alternate infinite ease-in-out;
    transition: --rz1 600ms ease-in-out;
}

body:hover p {
    --rz1: -350deg;
}

@keyframes custom-prop-o-rama {
    33% {
        --rx1: 20deg;
        --ry1: -303deg;
    }
    50% {
        --rx1: -20deg;
        --ry2: -30deg;
    }
    67% {
        --rx2: -200deg;
        --ry2: 130deg;
        --rz2: -350deg;
    }
}

div {
    --size: 50vmin;
    
    transform: 
        rotateX(var(--rx1))
        rotateY(var(--ry2))
        rotateZ(var(--rz1))
        rotateZ(var(--rz2))
        rotateX(var(--rx2))
        rotateY(var(--ry1));
}


p {
    --size: 36vmin;
    --border-size: 2.5vmin;
    --color: 188;
    
    transform:
        rotateX(var(--rx1))
        rotateY(calc(var(--ry1) * 2))
        rotateZ(var(--rz1))
        rotateY(var(--ry2));
}


span {
    --size: 24.5vmin;
    --border-size: 2vmin;
    --color: 36;
    
    transform:
        rotateX(var(--rx2))
        rotateY(var(--ry2))
        rotateZ(var(--rz2))
        rotateZ(var(--rz1))
        rotateX(var(--rx1))
        rotateY(var(--ry1));
}

效果如下:

Meterial设计的Ripple效果

使用CSS Houdini的自定义属性可以很容易实现Meterial设计中的Ripple效果

@Ana Tudor在Codepen就提供了CSS Houdini自定义属性实现Meterial设计中的Rripple效果:

制作饼图和圆形进度条

在《图解CSS: CSS渐变》一文中介绍了conic-gradient()repeating-conic-gradient()两个属性绘制饼图效果:

在这个基础上,把CSS Houdini的自定义属性运用到conic-gradient()就可以创建带有动效的饼图。@Ana Tudor在2018年发表的《Simple Interactive Pie Chart with CSS Variables and Houdini Magic》一文中详细介绍了下面这个效果的实现过程:

如果把CSS的mask特性和CSS Houdini自定义属性结合在一起,可以很容易地实现像下面这个Demo的圆环进度条效果:

其他效果

从上面的示例中我们可以知道,CSS Houdini自定义属性特性要比CSS原生自定义属性特性要强很多,使用CSS Houdini自定义属性特性和其他CSS特性结合起来,我们可以实现很多有趣的效果,甚至是以前很难实现的效果,有他之后都会变得非常容易。如果你想了解CSS Houdini自定义属性(或CSS Houdini)带来的更多效果,可以参阅 @Ana Tudor在Codepen提供的示例

小结

这篇文章主要介绍了CSS Houdini的CSS Properties and Values API Level 1特性。现在除了使用CSS.registerProperty()在JavaScript中注册自定义属性之外,还可以使用@property来注册自定义属,这两者之间只是注册自定义属性方式不同,但最终都是给Web开发者提供自定义属性。

CSS Houdini自定义属性和CSS原生自定义属性虽然在使用上非常相似,甚至很多都称其为CSS自定义属性或CSS变量,但他们之间还是有着本质区。CSS原生自定义属性始终是字符串,但CSS Houdini自定义属性对语法类型,初始值和是否继承都可以明确的指定。这样浏览器就知道自定义属性的类型等。

最后和大家一起分享了CSS Houdini自定义属性构建的一些Demo,特别向大家演示了以前很难实现的Web动效,有了CSS Houdini自定义属性之后,它们就变得非常容易。也就是说,有该特性之后,我们可以创建很多有意的效果。

最后希望这篇文章对大家有所帮助,如果你在这方面有相关的经验或建议,欢迎在下面的评论中与我们共享。