各位同学、大佬们好,我是wheatup,第一次在掘金发表文章,如有错误或不严谨的地方,请各位巨佬指正并海涵。
相信很多人都已经接触过CSS自定义属性(CSS Custom Properties)了,这也并非什么新鲜的概念。
但是很多人对于它的使用,也仅停留在对于全局变量的定义,用于规范页面的整体风格,例如:
:root {
--main-color: #369;
}
p {
color: var(--main-color);
}
但比起自定义CSS属性,如果项目中使用了预编译CSS(例如scss),一些人更倾向于使用其自带的变量系统,其语法更为简练:
$mainColor: #369;
p {
color: $mainColor;
}
但这些预编译CSS最终都会被编译成原生CSS,而这些所谓的变量也不过是单纯的复制粘贴为其定义的值,并不会实际起到变量的作用。
而CSS自定义属性与预编译CSS的变量不同,它们实际上就是一个变量,这个变量可以被 JavaScript、CSS Animation、CSS Transition 所控制。
如果灵活掌握CSS自定义属性,这将会颠覆你所学的所有对于CSS的认知,让你的CSS也可以接口化、自定义化。
案例:制作一个简单的进度条
先从一个简单的案例开始说起,例如我们需要制作一个进度条,如下图所示:

首先,我们定义一个HTML元素figure,并且给它一个class名为progress-bar。
<figure class="progress-bar"></figure>
对于CSS,我们暂时先将它的基础框架搭建出来:
.progress-bar {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 80vw;
height: 4rem;
border-radius: 5rem;
border: solid 2px #333;
background-color: #888;
overflow: hidden;
}
.progress-bar::before {
content: '';
display: block;
background-color: orangered;
position: absolute;
top: 0;
left: 0;
width: 50%;
height: 100%;
}
.progress-bar::after {
content: '50%';
z-index: 1;
color: white;
font-size: 1.25em;
}
这些都是一些基础的CSS,不作过度阐述,大致可以解释为如下:
- 使用了
flex布局让其所有元素居中。 - 使用了伪元素
::before负责渲染进度条的填充部分,其width为50%,也就是一半,背景颜色暂定为orangered,动态颜色我们之后再解决。 - 使用了伪元素
::after进行渲染进度条的文字部分,内容暂时强行指定为'50%'。
此时我们的进度条长这样:

目前而言,我们的进度条目前只停留在 50%。我们希望只用一个变量去控制进度条所有的行为,包括其填充的长度以及颜色。
这时候CSS自定义属性就派上用场了,我们暂且将它称之为--progress。
.progress-bar {
--progress: 0.25;
/* ... */
}
我们规定它将是一个 0 到 1 之间的值:
- 当它的值为
0时,进度条是空的,颜色为红色。 - 而当它的值为
1时,进度条将是满的,并且颜色为绿色。 - 而当它的值在
0到1以内时,进度条将填充对应的区间,并且颜色为红色到绿色之间。
控制进度条长度
为了让 --progress 可以控制进度条的长度,我们将 ::before 的 width 改写为:
.progress-bar::before {
/* ... */
width: calc(var(--progress) * 100%);
/* ... */
}
- 所有需要用到CSS自定义属性的地方,一律用
var()进行包裹,否则CSS将其无法识别。 var()可以用于计算calc()方法中。- 如果变量没有单位(即纯数字),用它乘以任何一个带单位的属性结果会获得其单位。
因为我们的 width 是一个百分比值,而 --progress 只是一个单纯的数字,我们需要使用 calc 乘以 100% 将其转化为对应的百分比值。
我们在 .progress-bar 中定义了 --progress 属性值为 0.25,所以计算结果应该为 width: 25%;,其实际效果如下:

大致目测了一下,应该是 25% 没错。至于目前的文字还是显示 50%,我们稍后再解决。
这时如果你去改动 --progress 的值,进度条的长度也会进行相应的变化,此时进度条的长度算是解决了。
控制进度条颜色
首先考虑一下,我们希望--progress 为 0 时,颜色应该为红色;--progress 为 1 时,颜色应该为绿色。在介于 0 到 1 之间的值,它的颜色应该也介于红色与绿色之间。
这时候,如果使用 RGB 去控制颜色的话,效果不会很理想,因为我们很难用一个值去控制三个颜色所对应的值,计算起来也异常头疼。
这里我推荐大家使用 HSL 进行颜色控制:
H(Hue): 色相S(Saturation): 饱和度L(Lightness): 亮度
我们可以想象一下,我们所需要的颜色的饱和度和亮度应该是不变的,变的只有其色相(红 ↔ 绿),如果我们可以将色相用--progress去控制,不就刚好解决了我们的需求了吗?
对于HSL颜色控制,大家可以去搜索一下其具体用法,这不是本篇文章讨论的重点,这里只进行简单的介绍。
H是一个角度值,区间为0deg到360deg之间。H为0deg时,颜色为红色。H为120deg时(三分之一)时,颜色为绿色。H为240deg时(三分之二)时,颜色为蓝色。S是一个百分比值,0%为无饱和度,100%为最大饱和度。L是一个百分比值,0%为黑色,100%为白色,只有介于两者之间才能看到色相和对比度。
所以我们可以得知我们需要 H 的值就是在 0deg 和 120deg 之间,这时我们套入一个公式,将 0 ~ 1 转化为 0deg ~ 120deg:
.progress-bar::before {
/* ... */
background-color: hsl(calc(0deg + var(--progress) * 120deg), 100%, 35%);
/* ... */
}
此时我们对比一下,当 --progress 为 0.25 、0.5、0.75时的状态:

这样我们的颜色就算是控制完成了,此这时如果你去改动 --progress 的值,进度条的长度和颜色都会进行相应的变化。
控制其他属性
我们如法炮制,使用 --progress 去控制 border 和 box-shadow等属性,这样我们的进度条就和本篇文章刚开始的效果并无二致了。
完整CSS代码:
.progress-bar {
--progress: 0.25;
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 80vw;
height: 4rem;
border-radius: 5rem;
border: solid 2px hsl(calc((30 + var(--progress) * 60) * 1deg), 100%, 15%);
box-shadow: 0px 0px 2rem hsla(calc((30 + var(--progress) * 60) * 1deg), 100%, 50%, var(--progress));
background-color: #888;
overflow: hidden;
}
.progress-bar::before {
content: '';
display: block;
background-color: hsl(calc(0deg + var(--progress) * 120deg), 100%, 50%);
position: absolute;
top: 0;
left: 0;
width: calc(var(--progress) * 100%);
height: 100%;
}
.progress-bar::after {
content: '50%';
z-index: 1;
color: white;
font-size: 1.25em;
}
至此我们实现了仅用一个css属性值 --progress 控制了该元素的各种属性值,一个简单的CSS属性封装就完成了。
控制文字显示
很遗憾,CSS自定义属性并不能控制伪元素的content,你会发现如下写法并没有什么卵用:
.progress-bar::after {
content: calc(var(--progress) * 100) '%';
/* ... */
}
这是因为 content 只接受字符串以及 attr() ,而CSS并没有将值转化为字符串的方法,所以我们只能从JavaScript下手了。
题外话,希望CSS X的到来可以修复此问题。
我们规定一个HTML Attribute,名为 data-progress,然后在使用JavaScript设置其CSS自定义属性 --progress 时,我们也一并设置其 data-progress:
<figure class="progress-bar" data-progress="50"></figure>
.progress-bar::after {
content: attr(data-progress) '%';
/* ... */
}
[...document.querySelectorAll('.progress-bar')].forEach(e => e.addEventListener('mousemove', ({offsetX, target}) => {
// progress,0到1之间的值
const progress = offsetX / target.clientWidth;
// 设置CSS自定义属性
target.style.setProperty('--progress', progress);
// 设置Attribute
target.setAttribute('data-progress', Math.abs((progress * 100)).toFixed(0));
}));
我们使用了鼠标位置可以控制 --progress 和 data-progress,这样一来我们就可以实时观察进度条的状态了:

*使用CSS Houdini给CSS自定义属性添加动画
如果你希望使用 transition 或者 animation 去给 --progress 添加动画,你可以在js中定义如下:
CSS.registerProperty({
name: '--progress',
syntax: '<number>',
inherits: false,
initialValue: '0'
});
如此一来浏览器便知道 --progress 是一个 number,可以正确地解析如下 keyframes,否则默认只能从 0 直接跳为 1,反之亦然。
@keyframes load {
from {
--progress: 0;
}
to {
--progress: 1;
}
}
鉴于CSS Houdini目前支持率和稳定性并不是特别好,这里也只做一下简单的概述,如果有兴趣请查阅相关文献。
结束语
这样一来,本篇文章所需涵盖的内容也大致都讲完了。如果觉得内容过多,可以试着自己多摆弄摆弄,比如多定义几个自定义属性,以及用它们控制更多的属性,一定会加深你对自定义属性的理解。
也感谢大家阅读到了最后,希望本文对大家的前端知识有所帮助,今后也会不定期地写类似关于前端知识的博客,与君共勉。
完整项目CodePen: codepen.io/wheatup/pen…