CSS自定义属性你知道多少

发布于 大漠

时至今日,CSS自定义属性对于CSS而言不是什么新特性了,它也被纳入到W3C的规范中,即**CSS Custom Properties for Cascading Variables Module Level 1**。在《CSS自定义属性》一文中我们对CSS自定义属性(变量)做过全面的介绍,如果你阅读过该文,你应该对CSS自定义属性会有一个基本性的了解以及如何在实际项目中使用该特性。那么再试问一下,你对CSS自定义属性知道多少呢?如果你无法清楚的回答清楚,请继续往下阅读。

CSS自定义属性概要

除了CSS语言之外的其他计算机语言都有变量的概念,但随着CSS预处理器语言的出现,开始引入了变量的概念,随之CSS也引入了变量的概念,但CSS中原生中的变量概念有两种说法:自定义属性变量

使用--前缀声明的属性被称之为CSS的自定义属性,通过var()函数引用的自定义属性又被称之为CSS的变量!

简而言之,在CSS的世界中,通过--前缀声明的属性既是自定义属性也是变量

CSS自定义属性的声明

CSS中使用--声明的属性被称为CSS自定义属性:

--customProperty: value

CSS自定义属性的声明和CSS预处理器中的变量的声明有所不同,在CSS中的自定义属性声明必须是在一个CSS选择器区块之中,比如说在根选择器:

:root {
    --customProperty: value
}

也可以是在独立的选择器:

selector {
    --customProperty: value
}

这两种方式不同之处是,在:root{}区块中声明的自定义属性是全局的,而在独立的选择器(非:root)中声明的自定义属性是局部的。

CSS的变量

在CSS中,我们可能在任意的属性中通过var()函数来引用已声明的自定义属性,在这个时候,该自定义属性又被称之为CSS变量。比如:

:root {
    --bgColor: #fff; // 它是一个自定义属性
}

html {
    background-color: var(--bgColor); //bgColor是一个变量
}

CSS自定义属性是动态的

CSS自定义属性和CSS预处理器中的变量还有一个最大的不同之处。那就是CSS自定义属性可以和JavaScript一起操作。换句话说,我们可以使用JavaScript中的.getPropertyValue.setProperty.removeProperty三个API对CSS自定义属性进行操作。

  • 使用.getPropertyValue可以获取自定义属性的值,比如.getPropertyValue(propertyName)
  • 使用.setProperty可以重置自定义属性的值,比如.setProperty(propertyName, value, priority)
  • 使用.removeProperty可以删除自定义属性,比如.removeProperty(propertyName)

CSS自定义属性可以用来做一些很酷的事情

CSS的自定义属性和CSS处理器中的变量有着很大的差异性:它不需要经过任何编译就可以在客户端运行,同时JavaScript可以很好的对其自己操作。另外,在实际开发中,CSS自定义属性可以做一些很酷的事情,可能有些事情会感兴趣。

颜色的作用域

最简单和最常见的例子就是颜色的作用域。比如像Bootstrap中的按钮,是通过不同的类名来设置不同的颜色,以区分颜色的作用域:

对于这样的场景,CSS自定义属性具有较高的灵活性和可扩展性,而且代码干净。比如,我们只需要在button中定义颜色所面的自定义属性,比如:

.btn {
    --btnColor: #5eb5ff;
    border: 1px solid var(--btnColor);
    color: var(--btnColor);
    
    &:hover {
        color: white;
        background-color: var(--btnColor);
    }
}

我们可以在不同的类中简单地改变自定义属性的值:

.btn-red {
    --btnColor: #ff6969;
}

.btn-green {
    --btnColor: #7ae07a;
}

.btn-gray {
    --btnColor: #555;
}

CSS颜色有多种表达方式,比如**CSS Color Module Level 3Level 4**中所述,除了颜色关键词还有十六进制、RGBRGBAHSLHSLA等方式来命名一个颜色。其中HSLHSLA对于程序化的颜色控制有着独特的优势

有了CSS自定义属性、HSL(或HSLA)和calc()的能力,我们可以基于CSS创建动态的主题。简单地说,使用纯CSS动态计算颜色值,比如@una的《Calculating Color: Dynamic Color Theming with Pure CSS》一文就详细介绍了这方面的原理。下面就是@una写的一个Demo:

我们可以将这特性用于自己的组件颜色的构建中。比如上面的按钮示例,可以通过--h--s--l--a来控制颜色。比如下面这个示例:

.button {
    background-color: background: hsl(var(--h), 100%, 50%);
}

.button--primary {
    --h: 233;
}

.button--secondary {
    --h: 200;
}

定制主题皮肤

CSS自定义属性的另一个最大用例就是定制主题皮肤。比如下面这个效果:

如果你想在你的业务中实现Dark Mode的效果,那么CSS自定义属性会让事情变得简单地说。

:root {
    --duration: 0.5s;
    --timing: ease;

    --bg-color-primary: #f0f0f0;
    --bg-color-secondary: #fff;
    --bg-color-tbleve3: #fafafa;
    --text-color-primary: #111;
    --text-color-secondary: #666;
    --text-auxiliary-color-primary: #999;
    --text-auxiliary-color-secondary: #ddd;
    --icon-filter_hover: invert(60%);

    color-scheme: dark light;
    supported-color-schemes: dark light;
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg-color-primary: #111;
        --bg-color-secondary: #222;
        --bg-color-tbleve3: #333;
        --text-color-primary: #ddd;
        --text-color-secondary: #aaa;
        --text-auxiliary-color-primary: #888;
        --text-auxiliary-color-secondary: #777;
        --icon-filter: invert(100%);
        --icon-filter_hover: invert(60%);
        --image-filter: grayscale(50%);
    }

    img[src*='.svg'] {
        filter: var(--icon-filter);
    }

    img:not([src*='.svg']) {
        filter: var(--image-filter);
    }
}

body {
    transition: color var(--duration) var(--timing), background-color var(--duration) var(--timing);
    background-color: var(--bg-color-primary);
    color: var(--text-color-primary);
}

使用CSS自定义属性来调整关键帧动效的变化

CSS的@keyframes可以显式的声明一个动画,该动画可以运用于任何元素之上。但可以在不同的元素上调整一些属性,比如animation-durationanimation-delay等可以调整动画效果的差异。比如,我们在.walk.run两个元素上都运用了breath这个动画效果,不同的是.run上的动画速度要比.walk快:

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

.walk {
    animation: breath 2s alternate;
}

.run {
    animation: breath 0.5s alternate;
}

每次我们重用一个动画时,它都会根据我们分配给它的属性做出不同的行为。所以,我们可以说一个动画基于它所应用的元素继承了它的行为。但是动画规则?我们回到breath动画中来,.walk元素的breath动效更慢,但它也需要做一些细节上的调整,所以将它的scale()值设置的要比.run更大,因为它需要更小,更频繁的呼吸。那么动画效果就需要创建两个:

@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自定义属性未出现的时候,这是一个较好的解决方案,但CSS自定义属性的到来可以给我们一个更好的解决方案。它请允许我们重用动画的属性和它的值。简单地说,就是继承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中是没有逻辑判断这么一说的。但借助CSS自定义属性,我们可以在一些特殊的场景模拟出真和假这样的场景。比如:

除了用10来模拟真假的场景之外,还可以实现非零值之间的切换,比如用于前面所说的颜色值的计算也是非常有用的:

来看一个这方面的实际用例:

有关于这方面更多的介绍可以阅读@Ana tudor的文章:

我也针对上面两篇文章做了些相关的整理,如果不想阅读英文的话,可以阅读《如何通过CSS自定义属性给CSS属性切换提供开关》一文。有关于这方面更多的Demo,还可以查看@Ana tudor在Codepen上整理的Demo集合

缓动函数的反转

熟悉CSS中animationtransition的同学,都知道这两个属性中都有缓动函数的概念,即animation-timin-functiontransition-timing-function对应的属性值。这两个属性常见的属性值主要有:linearease-inease-outease-in-out等。除了这几个还有贝塞尔曲线函数cubic-bezier()

从上图中可以看出,cubic-bezier()函数主要由两个点来控制,比如说点(x1, y1)(x2,y2),结合起来就是cubic-bezier(x1,y1,x2,y2)。而在animation中还有另一个属性animation-direction可以让动画反转(animation-direction: reverse)。

为了反转动画的缓动曲线,我们需要在它的轴上旋转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文件的大小

CSS自定义属性和其他的CSS处理器还有一个不同之处,CSS自定义属性可以在类选择器中修改,允许你创建抽象并减少CSS代码量。在CodyHouse框架中,使用.grid-gap-{size}来设置网格项目之间的间距:

.grid {
    display: flex;
    flex-wrap: wrap;

    > * {
        flex-basis: 100%;
    }
}

.grid-gap-xxxs {
    margin-bottom: calc(-1 * var(--space-xxxs));
    margin-left: calc(-1 * var(--space-xxxs));

    > * {
        margin-bottom: var(--space-xxxs);
        margin-left: calc(var(--space-xxxs));
    }
}

.grid-gap-xxs {
    margin-bottom: calc(-1 * var(--space-xxs));
    margin-left: calc(-1 * var(--space-xxs));

    > * {
        margin-bottom: var(--space-xxs);
        margin-left: calc(var(--space-xxs));
    }
}

.grid-gap-xs {
    margin-bottom: calc(-1 * var(--space-xs));
    margin-left: calc(-1 * var(--space-xs));

    > * {
        margin-bottom: var(--space-xs);
        margin-left: calc(var(--space-xs));
    }
}

.grid-gap-sm {
    margin-bottom: calc(-1 * var(--space-sm));
    margin-left: calc(-1 * var(--space-sm));

    > * {
        margin-bottom: var(--space-sm);
        margin-left: calc(var(--space-sm));
    }
}

.grid-gap-md {
    margin-bottom: calc(-1 * var(--space-md));
    margin-left: calc(-1 * var(--space-md));

    > * {
        margin-bottom: var(--space-md);
        margin-left: calc(var(--space-md));
    }
}

.grid-gap-lg {
    margin-bottom: calc(-1 * var(--space-lg));
    margin-left: calc(-1 * var(--space-lg));

    > * {
        margin-bottom: var(--space-lg);
        margin-left: calc(var(--space-lg));
    }
}

.grid-gap-xl {
    margin-bottom: calc(-1 * var(--space-xl));
    margin-left: calc(-1 * var(--space-xl));

    > * {
        margin-bottom: var(--space-xl);
        margin-left: calc(var(--space-xl));
    }
}

.grid-gap-xxl {
    margin-bottom: calc(-1 * var(--space-xxl));
    margin-left: calc(-1 * var(--space-xxl));

    > * {
        margin-bottom: var(--space-xxl);
        margin-left: calc(var(--space-xxl));
    }
}

.grid-gap-xxxl {
    margin-bottom: calc(-1 * var(--space-xxxl));
    margin-left: calc(-1 * var(--space-xxxl));

    > * {
        margin-bottom: var(--space-xxxl);
        margin-left: calc(var(--space-xxxl));
    }
}

如果使用CSS自定义属性来处理的话,代码量会少很多很多:

.grid {
    --grid-gap: 0px;
    display: flex;
    flex-wrap: wrap;

    > * {
        flex-basis: 100%;
    }
}

[class*="grid-gap"] {
    margin-bottom: calc(-1 * var(--grid-gap, 1em));
    margin-left: calc(-1 * var(--grid-gap, 1em));

    > * { 
        margin-bottom: var(--grid-gap, 1em); 
        margin-left: var(--grid-gap, 1em);
    }
}

.grid-gap-xxxxs { --grid-gap: var(--space-xxxxs); }
.grid-gap-xxxs  { --grid-gap: var(--space-xxxs); }
.grid-gap-xxs   { --grid-gap: var(--space-xxs); }
.grid-gap-xs    { --grid-gap: var(--space-xs); }
.grid-gap-sm    { --grid-gap: var(--space-sm); }
.grid-gap-md    { --grid-gap: var(--space-md); }
.grid-gap-lg    { --grid-gap: var(--space-lg); }
.grid-gap-xl    { --grid-gap: var(--space-xl); }
.grid-gap-xxl   { --grid-gap: var(--space-xxl); }
.grid-gap-xxxl  { --grid-gap: var(--space-xxxl); }
.grid-gap-xxxxl { --grid-gap: var(--space-xxxxl); }

复杂的计算

自定义属性对于存储计算值(来自calc()函数)非常方便,甚至可以从其他自定义属性计算这些值。比如,clip-path希望相对于已知路径来做一个计算,那么CSS自定义属性会容易得多。

.element {
    --top: 20%;
    --bottom: 80%;
    --gap: 1rem;
    --offset: calc(var(--gap) / 2);
}

.element::before {
    clip-path: polygon(
        calc(var(--top) + var(--offset)) 0,
        100% 0,
        100% 100%,
        calc(var(--bottom) + var(--offset)) 100%
    );
}

.element::after {
    clip-path: polygon(
        calc(var(--top) - var(--offset)) 0,
        calc(var(--bottom) - var(--offset)) 100%,
        0 100%,
        0 0
    );
}

响应式网格

对于前端开发人员来说,响应式设计不是什么新东西,创建一个具有响应式的网格系统也不是件难事。CSS创建响应式网格方法也很多,但是CSS自定义属性可以使复杂的网格布局变得更容易管理。假设我们有一个八列的网格,我们希望在一个特定的断点将其改为十二列的网格。使用CSS自定义属性,我们可以这么做:

:root {
    --noOfColumns: 8;
}

@media (min-width: 60em) {
    :root {
        --noOfColumns: 12;
    }
}

.grid {
    display: grid;
    grid-template-columns: repeat(var(--noOfColumns), 1fr);
}

小结

时至今日CSS自定义属性已经得到很多主流浏览器的支持,而且在实际场景的运用越来越频繁。她能受到开发者的喜受,主要是因为CSS自定义属性和以往CSS处理器中的变量有着与众不同的特性。在这篇文章中,整理了CSS自定义属性的基本特性以及一些使用案例。事实上还有很多好的案例和特性没有被挖掘出来,如果你在这方面有相关的经验和优秀的案例,欢迎在下面的评论中分享出来。