CSS自定义属性的使用实例

发布于 大漠

过去一年来花了不少时间来研究CSS自定义属性相关的特性,而且在站上也发了多篇有关于CSS自定义属性相关的教程,其中有关于CSS自定义属性,也有涉猎CSS Houdini自定义属性相关的。但我想很多同学除了希望了解CSS自定义属性理论相关的知识之外还更想了解CSS自定义属性能做什么吧。在这篇文章中,我将整理一些有关于CSS自定义属性在实际开发中能做什么,并会在文章中提供相应的一些使用实际。希望能通过具体的实例帮助大家更好的理解和使用CSS自定义属性。

组件优化

Web组件(或者说UI组件)在Web中是最可见之一,使用CSS自定义属性可以让组件的代码变得更简单。拿Bootstrap的button举例

.btn-primary的CSS代码如下:

.btn {
    color: #212529;
    background-color: transparent;
    border-color: transparent;
}

.btn:hover {
    color: #212529;
}

.btn:focus {
    box-shadow: 0 0 0 0.25rem rgba(13,110,253,.25);
}

.btn-primary {
    color: #fff;
    background-color: #0d6efd;
    border-color: #0d6efd;
}

.btn-primary:hover {
    color: #fff;
    background-color: #0b5ed7;
    border-color: #0a58ca;
}

.btn-primary:focus {
    color: #fff;
    background-color: #0b5ed7;
    border-color: #0a58ca;
    box-shadow: 0 0 0 0.25rem rgba(49,132,253,.5);
}

.btn-primary:active {
    color: #fff;
    background-color: #0a58ca;
    border-color: #0a53be;
}

不难发现,button在默认状态,:active:focus:hover状态,变化的只是colorbackground-colorborder-colorbox-shadow等属性。如果我们换作CSS自定义属性来构建的话,代码会变得更简称:

root:{
    --btn-color: #212529;
    --btn-hover-color: #212529;
    --btn-bg-color: transparent;
    --btn-border-color: transparent;
    --btn-box-shadow: 0 0 0 0.25rem rgba(13,110,253,.25);
}

.btn {
    display: inline-flex;
    justify-content: center;
    align-items: center;
    font-weight: 400;
    line-height: 1.5;
    cursor: pointer;
    user-select: none;
    padding: .375rem .75rem;
    font-size: 1rem;
    border-radius: .25rem;
    transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}

.btn:focus {
    outline: 0;
}

.btn {
    color: var(--btn-color);
    background-color: var(--btn-bg-color);
    border: 1px solid var(--btn-border-color);
}

.btn:hover {
    color: var(--btn-hover-color);
}

.btn:focus {
    box-shadow: 0 0 0 0.25rem var(--btn-box-shadow);
}

在不同状态,比如.btn-primary.btn-secondary下只需要调整相应的自定义属性的值即可:

.btn-primary {
    --btn-color: #fff;
    --btn-bgcolor: #0a58ca;
    --btn-border-color: #0a58ca;
    --btn-box-shadow: rgba(49, 132, 253, 0.5);
    --btn-active-bgcolor: #0a58ca;
    --btn-active-border-color: #0a53be;
    --btn-hover-color: #fff;
    --btn-hover-bgcolor: #0b5ed7;
    --btn-hover-border-color: #0a58ca;
}

效果如下:

你可能已经发现了,在自定义属性的模式下,我们只需在相应的状态下调整自定义属性的值,不需要在代码中写CSS的属性:

使用CSS自定义属性,是不是比以前省事多了。这样做并不是说CSS自定义属性帮助开发者节约了多少代码或更易于维护,其真正的价值是:自定义属性让我们能够共享代码,让其他开发者可以重用或定制。比如说,我们可将一个button可变参数提取出来,并将这些参数设置为CSS自定义属性,那么就可以很好的调整(自定义地构建个性化)按钮的UI效果。

控制组件大小

在CSS Frameworks中很多组件都提供了相应的类名来控制组件大小,比如我们熟悉的Bootstrap中的按钮组件,有btn-lgbtn-sm类来调整组件的padding值,CSS Frameworks新起之秀Tailwindcss提供了更多类名来调整组件大小:

简单地说,通过调整组件的padding可以非常容易调整组件内距从而改变组件大小:

但往往影响组件大小并不仅仅是因为其 尺寸属性 (比如paddingwidthheightfont-size等)的设置,还和 属性取值的单位 也有紧密关系。比如,使用em单位,可以很好的调整组件组件大小:

em相似的相对单位还有remvwvhvminvmax以及%

而有了CSS自定义属性之后,我们可以更灵活的控制组件的大小:

正如上图所示,我们可以其于CSS自定义属性来调整font-size的值来改变组件大小,不过该方式有一个前提条件,那就是涉及到控制组件大小的属性(具有<length>的属性,比如widthpadding等)应该使用em来做单位(rem勉强也行)。我们使用这些方法来构建上图这样的卡片组件。

实现该效果最为关键的是在html中设置了font-size值为%值,它相对于16px做计算,同时我们在.card上设置的font-size值是rem值,它相对于htmlfont-size计算,另外在侧边栏aside调整自定义属性--fs的值,从而影响其font-size值的大小:

:root {
    --fs: 1rem;
    --root-fs: 100%;
}

html {
    font-size: var(--root-fs);
}

.card {
    font-size: var(--fs);
}

aside {
    --fs: 0.65rem;
}

另一个要点是,组件上的元素属性的值单位采用的都是em为单位,而em又是相对于自己font-size值计算,如果元素自已并未显式设置font-size,则会继承其父元素(祖先元素)的font-size

.card {
    max-width: 25em;
    box-shadow: 0 0 0.25em -0.15em rgba(255, 255, 255, 0.65);
    border-radius: 0.25em;
    border: 0.025em solid rgba(255, 255, 255, 0.65);
}
.card__thumbnail {
    border-radius: 0.275em 0.275em 0 0;
    overflow: hidden;
}

.card__thumbnail img {
    border-radius: 0.275em 0.275em 0 0;
}

.card__body {
    font-size: 1.125em;
    padding: 1em 1.25em;
}

.card__title {
    font-size: 1.5em;
    margin: 0 0 0.5em;
}

.card__content {
    font-size: 1.025em;
}

.card__footer {
    padding: 0.6em 1.25em 1.2em;
}

如果你未接触过值和单位之间的计算关系,建议你花点时间阅读下面相关的教程:

CSS自定义属性给颜色使用带来的变化

在Web运用中很多时候希望能很好的实现换肤效果。比如下面这个录屏效果,用户调整颜色时对应的插画颜色也会有所调整:

CSS自定义属性结合JavaScript可以很好的实现这样的效果。复制unDraw中的一个插画,查看代码,我们可以发现SVG中图形中使用fill来填充图形颜色:

在SVG的元素上稍作调整,比如添加类名:

<path d="M980.94772,659.04355h-743a1,1,0,0,1,0-2h743a1,1,0,0,1,0,2Z" transform="translate(-218.05228 -135.88181)" fill="#3f3d56" class="seat__fill"></path>

并在对应的CSS添加样式代码:

:root {
    --primary-fill-color: #6c63ff;
    --secondary-fill-color: #2f2e41;
    --leaf-fill-color: #f2f2f2;
    --leaf-fill-color-2: #e6e6e6;
    --seat-fill-color: #3f3d56;
}

.primary__fill {
    fill: var(--primary-fill-color);
}

.secondary__fill {
    fill: var(--secondary-fill-color);
}

.leaf__fill {
    fill: var(--leaf-fill-color);
}

.leaf__fill--secondary {
    fill: var(--leaf-fill-color-2);
}

.seat__fill {
    fill: var(--seat-fill-color);
}

调整对应的CSS自定义属性的值就可以调整SVG图形的颜色:

为此,我们使用JavaScript操作CSS自定义属性相关的API可以让用户操作颜色控制面板来调整插画的颜色:

const svgEle = document.querySelector("svg");

const inputColors = document.querySelectorAll('input[type="color"]');

inputColors.forEach((element) =>
    element.addEventListener("input", (evt) => {
        svgEle.style.setProperty(`--${evt.target.id}`, `${evt.target.value}`);
        console.log(evt.target.value);
    })
);

请尝试调整示例中颜色,你将看到的效果如下:

这种方式用于更换SVG图标也是非常实用。其中还需借助CSS的currentColor特性和SVG的fill的值相结合:

<svg fill="currentColor">
    <path fill="currentColor" />
</svg>

此时,color的值将会影响Icon的flll值。将CSS自定义属性的和color结合起来:

:root {
    --h: 180;
    --s: 50;
    --l: 50;
}

.icon__container {
    color: hsl(var(--h), calc(var(--s) * 1%), calc(var(--l) * 1%));
}

.icon__container:nth-child(1) {
    --h: 232;
}

.icon__container:nth-child(2) {
    --s: 22;
}

.icon__container:nth-child(3) {
    --l: 60;
}

效果如下:

该原理也常用于Web换肤中,比如@Jonathan Harrell使用CSS自定义属性实现的一个简单的皮肤编辑器

基于这个特性,结合CSS新的媒体查询特性prefers-color-scheme可以轻易实现iOS的暗黑模式的效果

有关于CSS实现暗黑模式的效果,更多的介绍可以阅读:

CSS自定义属性在颜色上的使用,除了上面提到的这几个常用场景之外,还可以用来构建颜色面板:

上图是设计师设计的系统化的颜色表:

在Web体系中,CSS中给颜色相关的属性(比如colorborder-color等)设置值时可以根据不同颜色空间来设置。简单地说,我们可以通过hsl()hwb()lab()lch()等方式设置颜色值。就拿hsl()来说吧,可以改变其中一个值得到不同的颜色:

将CSS自定义属性特性引入进来,构建系统化的颜色表会变得更为容易。比如下面示例中左侧的颜色列表,亮度L是50%,饱和度S按10%递增;右侧的颜色列表,亮度L是按10%递增,饱和度S是100%;用户拖动示例中的滑块,可以改变色相H的值:

在实际案例中,可以像下面这样使用:

:root {
    --primary-h: 221;
    --primary-s: 71%;
    --primary-b: 48%;
}

.button {
    background-color: hsl(var(--primary-h), var(--primary-s), var(--primary-b));
    transition: background-color 0.3s ease-out;
}

.button:hover {
    --primary-b: 33%;
}

在颜色中使用色相选色有很多好处,这也是hsl的主要优势之一。在使用颜色的时候,CSS自定义属性来定义HSL中的每个值的优势从上面的示例中可以体现出来。除此之外,还可以快速构建补色位。补色位于色轮上的对面。因此,如果你从一个颜色开始,你想得到它的补色,只需要在色相的值上增加180°

:root {
    --h: 257;
    --complementary-h: calc(var(--h) + 180); /* calc(var(--h) - 180)*/
    --s: 26%;
    --l: 42%;

    --primary-color: hsl(var(--h), var(--s), var(--l));
    --complementary-color: hsl(var(--complementary-h), var(--s), var(--l))
}

除此之外,还可以像下图所示,通过加(或减)120°来创建三元色方案,也可以用30°分隔色调来创建类似的色彩组合:

可以像下面这样使用CSS自定义属性来操作颜色:

:root {
    /* 初始化颜色的hsl值 */
    --h: 180;
    --s: 50%;
    --l: 50%;

    /* 设置互补色的分割粒度 */
    --splitter: 30;

    /* 改变深色和亮色变体的亮度和饱和度 */
    --shader: 15%;

    /* 计算每个颜色操作的色调值 */
    --hueNext: calc(var(--h) + 30);
    --huePrev: calc(var(--h) - 30);
    --hueComp: calc(var(--h) + 180);
    --hueTriad1: calc(var(--h) + 120);
    --hueTriad2: calc(var(--h) + 120 + 120);
    --hueTet1: calc(var(--h) + 90);
    --hueTet2: calc(var(--h) + 90 + 90);
    --hueTet3: calc(var(--h) + 90 + 90 + 90);  
    --hueSplitComp1: calc(var(--hueComp) + var(--splitter));
    --hueSplitComp2: calc(var(--hueComp) - var(--splitter));

    /* 计算每个颜色操作的色调值 */
    --satDarker: calc(var(--s) + var(--shader));
    --satLighter: calc(var(--s) - var(--shader)); 

    /* 计算深色和亮色变体的亮度值 */
    --lightDarker: calc(var(--l) - var(--shader));
    --lightLighter: calc(var(--l) + var(--shader));  

    /* 计算主色及其深色和亮度的变体 */
    --mainColor: hsl(var(--h), var(--s), var(--l));
    --mainColorDarker: hsl(var(--h), var(--satDarker), var(--lightDarker));
    --mainColorLighter: hsl(var(--h), var(--satLighter), var(--lightLighter));

    /* 计算相邻的颜色及其深色和亮色的变体 */
    --nextColor: hsl(var(--hueNext), var(--s), var(--l));
    --nextColorDarker: hsl(var(--hueNext), var(--satDarker), var(--lightDarker));
    --nextColorLighter: hsl(var(--hueNext), var(--satLighter), var(--lightLighter));
    --prevColor: hsl(var(--huePrev), var(--s), var(--l));
    --prevColorDarker: hsl(var(--huePrev), var(--satDarker), var(--lightDarker));
    --prevColorLighter: hsl(var(--huePrev), var(--satLighter), var(--lightLighter));    

    /* 计算补色及其深色和亮色的变体 */
    --compColor: hsl(var(--hueComp), var(--s), var(--l));
    --compColorDarker: hsl(var(--hueComp), var(--satDarker), var(--lightDarker));
    --compColorLighter: hsl(var(--hueComp), var(--satLighter), var(--lightLighter));  

    /* 计算相似的颜色(1 & 2)和它们的深色和亮色的变体 */
    --analgColor1: var(--nextColor);
    --analgColor1Darker: var(--nextColorDarker);
    --analgColor1Lighter: var(--nextColorLighter);
    --analgColor2: var(--prevColor);
    --analgColor2Darker: var(--prevColorDarker);
    --analgColor2Lighter: var(--prevColorLighter);

    /* 计算三和色(1 & 2)和它们的深色和亮色的变体 */
    --triadColor1: hsl(var(--hueTriad1), var(--s), var(--l));
    --triadColor1Darker: hsl(var(--hueTriad1), var(--satDarker), var(--lightDarker));
    --triadColor1Lighter: hsl(var(--hueTriad1), var(--satLighter), var(--lightLighter));  
    --triadColor2: hsl(var(--hueTriad2), var(--s), var(--l));
    --triadColor2Darker: hsl(var(--hueTriad2), var(--satDarker), var(--lightDarker));
    --triadColor2Lighter: hsl(var(--hueTriad2), var(--satLighter), var(--lightLighter)); 

    /* 计算四次元颜色(1-3)和它们的深色和亮色的变体 */
    --tetColor1: hsl(var(--hueTet1), var(--s), var(--l));
    --tetColor1Darker: hsl(var(--hueTet1), var(--satDarker), var(--lightDarker));
    --tetColor1Lighter: hsl(var(--hueTet1), var(--satLighter), var(--lightLighter));  
    --tetColor2: hsl(var(--hueTet2), var(--s), var(--l));
    --tetColor2Darker: hsl(var(--hueTet2), var(--satDarker), var(--lightDarker));
    --tetColor2Lighter: hsl(var(--hueTet2), var(--satLighter), var(--lightLighter));    
    --tetColor3: hsl(var(--hueTet3), var(--s), var(--l));
    --tetColor3Darker: hsl(var(--hueTet3), var(--satDarker), var(--lightDarker));
    --tetColor3Lighter: hsl(var(--hueTet3), var(--satLighter), var(--lightLighter));  

    /* 计算分色补色(1 & 2)和它们的深色和亮色的变体 */
    --splitCompColor1: hsl(var(--hueSplitComp1), var(--s), var(--l));
    --splitCompColor1Darker: hsl(var(--hueSplitComp1), var(--satDarker), var(--lightDarker));
    --splitCompColor1Lighter: hsl(var(--hueSplitComp1), var(--satLighter), var(--lightLighter));  
    --splitCompColor2: hsl(var(--hueSplitComp2), var(--s), var(--l));
    --splitCompColor2Darker: hsl(var(--hueSplitComp2), var(--satDarker), var(--lightDarker));
    --splitCompColor2Lighter: hsl(var(--hueSplitComp2), var(--satLighter), var(--lightLighter));  
}

@una的《Calculating Color: Dynamic Color Theming with Pure CSS》一文也详细介绍了CSS自定义属性来计算颜色。文中也提供了一个类似上例的Demo:

A11Y系列中多次提到颜色对于Web可访问性有着直接的影响:

正如《A11Y 101:颜色对比度和Web可访问性》文中所提到的,CSS自定义属性可以很好的计算颜色对比度,实现具有可访问性的颜色:

:root {
    --h: 80;
    --s: 20;
    --l: 60;
    --threshold: 65;
    --border-threshold: 80;
}

.button {
    --hue: var(--h);
    --switch: calc((var(--l) - var(--threshold)) * -100%);
    --border-light: calc(var(--l) * 0.7%);
    --border-alpha:calc((var(--l) - var(--border-threshold)) * 10);
    
    background: hsl(var(--hue), calc(var(--s) * 1%), calc(var(--l) * 1%));
    color: hsl(0, 0%, var(--switch));
    border:.2em solid hsla(var(--hue), calc(var(--s) * 1%), var(--border-light), var(--border-alpha));
}

.button--secondary {
    --hue: calc(var(--h) + 60);
}

按比例调整元素尺寸大小

熟悉设计软件的同学都应该知道,我们在绘制图形的时候,按住shift键拖动图形的角标可以让图形按宽高比进行缩放(调整元素尺寸大小),或者像Sketch的设计软件中,在WH之间的锁锁住宽高比,然后随意调整WH都可以让元素按宽高比调整尺寸大小:

而在CSS中,aspect-ratio属性可以让我们轻易地实现元素尺寸大小按宽高比来调整。比如下面这个示例:

:root {
    --ratio-w: 1;
    --ratio-h: 1;
    --ratio: calc(var(--ratio-w) / var(--ratio-h));
    --width: 10rem;
    --height: 10rem;
}

.box {
    width: var(--width);
    aspect-ratio: var(--ratio);
}

.box2 {
    heght: var(--height);
    aspect-ratio: var(--ratio);
}

aspect-ratio是众多CSS新特性之一,还未得到主流浏览器的支持。不过我们可以使用CSS自定义属性来实现相似的功能。比如说我们希望元素的尺寸按照4:3(即.75)进行缩放,只需要知道元素的width,我们就可以根据该比率计算出元素的height

.rect {
    --width: 400px;
    --aspect-ratio: .75;
    width: var(--width);
    height: calc(var(--width) * var(--aspect-ratio)); /* 300px */
}

如果是一个动态比例:

.rect {
    --ratio-w: 4;
    --ratio-h: 3;
    --aspect-ratio: calc(var(--ratio-w) / var(--ratio-h));
    --width: 400;
    width: calc(var(--width) * 1px);
    height: calc(var(--width) * 1px / var(--aspect-ratio));
}

Grid布局+响应式布局

在一些布局场景中,使用CSS Gridminmax()函数repeat()函数、fr单位auto-fit以及auto-fill的结合可以不再依赖媒体查询就可以很好的实现响应式布局

.container {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 10px;
}

CSS自定义属性在该场景中也非常有用。想象一下,你想让一个网格容器根据定义的首先宽度来显示它的网格项目(比如上面示例中的300px)。在未使用CSS自定义属性时,你可能需要为每个变化创建一个类和重复使用上面的代码:

.container {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 10px;
}

.container.container__2 {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(500px, 1fr));
    gap: 10px;
}

我们可以使用CSS自定义属性来替代定义首先宽度,这样会让代码变得更简洁,也更易于维护。比如:

:root {
    --grid-item: 300px;
}

.container {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(var(--grid-item), 1fr));
    gap: 10px
}

.contaner__card {
    --grid-item: 500px;
}

同样的,还可以把自定义属性用于gap属性上:

:root {
    --grid-item: 300px;
    --gap: 10px;
}

.container {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(var(--grid-item), 1fr));
    gap: var(--gap);
}

.contaner__card {
    --grid-item: 500px;
    --gap: 20px;
}

你可以尝试着调整下面Demo中的卡片宽度(网格定义的首先宽度)和卡片间距的值,但看布局的变化:

使用CSS自定义属性来存储值

我们平时在编写CSS代码的时候,有些属性的值过于偏长,比如像CSS的渐变

.linear-gradent {
    background-image: linear-gradent(to right, #09f, #f90);
}

.repeating-conic-gradient{
    background-image: repeating-conic-gradient(to right,#ff8a00 10%,#e52e71 20%)
}

.radial-gradient {
    background-image: radial-gradient(#ff8a00, #e52e71);
}

.repeating-radial-gradient {
    background-image: repeating-radial-gradient(circle, #ff8a00 10%, #e52e71 20%);
}

.conic-gradient {
    background-image: conic-gradient(#ff8a00, #e52e71);
}

.repeating-conic-gradient {
    background-image: repeating-conic-gradient(#ff8a00 10%, #e52e71 20%);
}

如果你有一个在整个系统中使用的渐变,那将它存储到一个CSS自定义属性中,可以让你在多次使用时变得更容易,比如:

:root {
    --linear-gradient: linear-gradent(to right, #09f, #f90);
}

.linear-gradient {
    background-image: var(--linear-gradient);
}

甚至还可以把其中的参数用CSS自定义属性来描述,比如:

.element {
    --angle: 150deg;
    --gradient-color: #235ad1, #23d1a8;
    --linear-gradient: linear-gradient(var(--angle), var(--gradient-color));
    background-image: var(--linear-gradient);
}

.element.inverted {
    --angle: -150deg;
}

.element.change-color {
    --gradient-color: red, yellow, lime, aqua, blue, magenta, red;
}

这样一来,只需要改变渐变中的某个参数就可以得到新的渐变效果,而且不需要重新写整个渐变的样式代码。

另外,在CSS中有很多属性都可以简写在一起(速记属性),比如我们熟悉的backgroundanimationtransitionmask等属性都是,比如:

animation: 
     <duration-time> || <timing-function> || <delay-time> || <single-animation-iteration-count> || <single-animation-direction> || <single-animation-fill-mode> || <single-animation-play-state> || [ none | <keyframes-name> ]

使用自定义属性,可以非常轻松地调整速记属性中的任意一个值:

.ani {  
    animation: 
        var(--animationName, pulse) 
        var(--duration, 2000ms) 
        var(--timingFunctioin, linear)
        var(--delay, 0ms)
        var(--iterationCount, infinite)
        var(--animation-drectioin, alternate)
        var(--fillMode, forwards)
}

.ani.faster {  
    --duration: 500ms;
}

.ani.shaking {  
    --animationName: shake;
}

自定义属性的计算

自定义属性还可以用来存储计算值(来自calc()函数),这些值本身甚至可以从其他自定义属性中计算出来。其实前面的示例已经有其身影,比如颜色的计算就用到了calc()

比如下面这个简单的示例,只需要调整--size的大小,就能看到用户头像也跟着变化:

.avatar {
    display: inline-flex;
    justify-content: center;
    align-items: center;

    --size: 1;
    width: calc(var(--size) * 30px);
    height: calc(var(--size) * 30px); /* 或aspect-ratio: 1/1 */
}

将CSS自定义属性和calc()结合起来,我们还可以实现以前认为比较难的布局效果,比如下图演示的效果:

实现上图效果仅仅几行代码即可:

:root {
    --margin: 20px;
    --aspect-width: 4;
    --aspect-height: 3;
    --constant: calc(
        var(--margin) * var(--aspect-width) / (var(--aspect-height) * 3)
    );
}

.thumbnails {
    padding: var(--margin);
}

.thumbnails__aside {
    margin-right: var(--margin);
    width: calc(2 / 3 * (100% - var(--margin)) + var(--constant));
}

.right {
    width: calc(1 / 3 * (100% - var(--margin)) - var(--constant));
}

.thumbnails__aside--bottom {
    margin-top: var(--margin);
}

甚至还可以使用它们的结合来构建反转一个二次贝塞尔曲线函数cubic-bezier()。为了反转动画的缓动曲线,我们需要在它的轴上旋转180度,找到一个全新的坐标。

如果缓动曲线的初始坐标为x1, y1, x2, y2,那么反转之后的坐标即为(1-x2), (1-y2), (1-x1), (1-y1)。既然知道了基本原理之后,我们同样可以借助CSS自定义属性,用代码来表示:

:root {
    --x1: 0.45;
    --y1: 0.25;
    --x2: 0.6;
    --y2: 0.95;

    --originalCurve: cubic-bezier(var(--x1), var(--y1), var(--x2), var(--y2));
}

根据上面的公式,可以计算出反转后的缓动曲线:

:root {
    --reversedCurve: cubic-bezier(calc(1 - var(--x2)), calc(1 - var(--y2)), calc(1 - var(--x1)), calc(1 - var(--y1)));
}

为了更易于理解,把上面的代码稍作调整:

:root {
    /* 原始坐标值 */
    --x1: 0.45;
    --y1: 0.25;
    --x2: 0.6;
    --y2: 0.95;

    --originalCurve: cubic-bezier(var(--x1), var(--y1), var(--x2), var(--y2));

    /* 反转后的坐标值 */
    --x1-r: calc(1 - var(--x2));
    --y1-r: calc(1 - var(--y2));
    --x2-r: calc(1 - var(--x1));
    --y2-r: calc(1 - var(--y1));

    --reversedCurve: cubic-bezier(var(--x1-r), var(--y1-r), var(--x2-r), var(--y2-r));
}

最后来看一个@Michelle Barker的《Reversing an Easing Curve》文中提供的一个示例:

CSS自定义属性给动效带来的变化

先来看一个有关于CSS动效的简单示例:

@keyframes breath {
    from {
        transform: scale(0.5);
    }
    to {
        transform: scale(1.5);
    }
}

.walk {
    animation: breath 2s alternate;
}

.run {
    animation: breath 0.5s alternate;
}

使用@keyframes创建了一个breath动效(呼吸灯效果),并且在.walk.run上都使用了这个breath动效,只不过是在时间上做了一些调整,.walk的持续时间比.run的长。事实上,.walk执行breath的速度较慢,但它更需要一个更深的呼吸灯效果,为此需要重新创建一个新动效:

@keyframes breath {
    from {
        transform: scale(0.5);
    }
    to {
        transform: scale(1.5);
    }
}

@keyframes breathDeep {
    from {
        transform: scale(0.3);
    }
    to {
        transform: scale(1.7);
    }
}

.walk {
    animation: breathDeep 2s alternate;
}

.run {
    animation: breath 0.5s alternate;
}

但将CSS自定义属性引入进来,会有一个更好的解决方案:

@keyframes breath {
    from {
        transform: scale(var(--scaleStart));
    }
    to {
        transform: scale(var(--scaleEnd));
    }
}

.walk {
    --scaleStart: 0.3;
    --scaleEnd: 1.7;
    animation: breath 2s alternate;
}

.run {
    --scaleStart: 0.8;
    --scaleEnd: 1.2;
    animation: breath 0.5s alternate;
}

还可以使用CSS自定义属性来改变控制动效的其他参数,比如改变animation-delay。比如下面这个错开的动效:

div {
    background-color: #09f;
    opacity: 0;
}

.ani {
    --delay: calc(var(--i, 1) * 100ms);
    animation: fadeIn 1000ms var(--delay) forwards;
}

@keyframes fadeIn {
    100% {
        opacity: 1;
    }
}

const buttonHander = document.querySelector("button");
const rects = document.querySelectorAll("div");

rects.forEach((element, index) => {
    element.style.setProperty("--i", index + 1);
});

buttonHander.addEventListener("click", () => {
    rects.forEach((element) => {
        element.classList.toggle("ani");
    });
});

上面演示的都是些简单地示例,其实CSS自定义属性,可以帮助我们实现很多复杂的动效,比如@Jhey的Demo

@Ana Tudor的Delete 2020, insert 2021 (pure CSS):

虽然CSS自定义属性给CSS带来很多优势,但也有一定的缺陷,比如说,CSS自定义属性在@keyframes规则中使用时并不能被动画化:

If you have read the spec for CSS variables, you might read the term animation-tainted. The idea is that when using a CSS variable inside a @keyframes rule, it can’t be animated.

比如下面这个示例:

.box {
    width: 50px;
    height: 50px;
    --offset: 0;
    border-radius: 5px;
    background-color: #09f;

    transform: translateX(var(--offset));
    animation: moveBox 2s linear infinite;
}

@keyframes moveBox {
    0% {
        --offset: 0;
    }
    50% {
        --offset: 300px;
    }
    100% {
        --offset: 600px;
    }
}

虽然蓝色盒子在移动,但并没有过渡的效果:

不过CSS Houdini的 @property可以改变这种现象

@property --offset {
    syntax: "<length-percentage>";
    inherits: true;
    initial-value: 0px;
}

使用@property给动效带来的变化,再来看两个示例,先来看 @lonekorean写的一个Demo,模拟拖动颜色滑块,得到一个新的RGB颜色

@una在她的《Randomized Selective Color: A Post List Study》文章也提详细介绍了这方面的特性:

CSS自定义属性切换技巧

在上一篇文章《CSS自定义属性你知多少》中提到CSS自定义属性可以让我们实现一些逻辑判断的功能,比如真和假,10之间的切换,甚至是与或非的运算。有关于这方面的介绍,请移步阅读@Ana Tudor的教程:

最近@Chris Coyier的《The CSS Custom Property Toggle Trick》和 @Lea Verou的《The -​-var: ; hack to toggle multiple values with one custom property》提出另外一个有意思的东东,即 CSS自定义属性可以实现多个值的切换

来看@Lea Verou文章中提供的一个案例:

button {
    padding: 0.6em 1em;
    border-radius: 0.2em;
    color: white;
    font: 600 100%/1 sans-serif;
    cursor: pointer;
    
    --is-raised: ; /* off by default */
    border: 1px solid var(--is-raised, rgb(0 0 0 / 0.1));
    background: var(
        --is-raised,
        linear-gradient(hsl(0 0% 100% / 0.3), transparent)
        )
        hsl(200 100% 50%);
    
    box-shadow: var(
        --is-raised,
        0 1px hsl(0 0% 100% / 0.8) inset,
        0 0.1em 0.1em -0.1em rgb(0 0 0 / 0.2)
    );
    text-shadow: var(--is-raised, 0 -1px 1px rgb(0 0 0 / 0.3));
}

button:hover {
    background-color: hsl(200 100% 40%);
}

button:active {
    box-shadow: var(--is-raised, 0 1px 0.2em black inset);
}

#foo {
    --is-raised: initial; /* turn on */
}

示例代码中出现一个--is-raised: ;这样的自定义属性设置,即--is-raised的值是一个空值(空字符串的值),但这是一个有效的自定义属性。而且这个自定义属性出现在多个属性中,比如borderbackgroundbox-shadowtext-shadow中。拿其中一个属性来举例:

button {
    --is-raised: ;
    border: 1px solid var(--is-raised, rgb(0 0 0 / 0.1));
}

#foo {
    --is-raised: initial;
}

刚才提到过,--is-raised: ;是一个有效的自定义属性,这样一来,border: 1px solid var(--is-raised, rgba(0 0 0 / .1))中的var(--is-raised, rgba(0 0 0 / .1))取的就是一个空值,也就是说border: 1px solid var(--is-raised, rgba(0 0 0 / .1))等同于border: 1px solid,也就是border: 1px solid currentColor,这个时候按钮的边框颜色是白色:

--is-raised: initial;时,它是一个无效的值,这个时候var(--is-raised, rgba(0 0 0 / .1))var()的第二个参数,即rgba(0 0 0 / .1),对应的border就变成border: 1px solid rgba(0 0 0 / .1)

其他属性也是如此。最终效果如下:

小结

这篇文章主要向大家演示了一些CSS自定义属性在实际项目中可以使用的场景,并且通过这些示例能更好的向大家阐述CSS自定义属性的使用以及她自身的魅力。其实CSS自定义属性还有很多好玩的,这里没有一一展示,如果你那有更好的玩的示例,欢迎在下面的评论中与我们一起共享。