大多数编程语言都支持变量。但遗憾的是,CSS从一开始就缺乏对原生变量的支持。如果写CSS的话,那是没有变量的,除非你使用像Sass这样的CSS处理器。
变量是Sass这样处理器的一个非常有用的特性之一。这也是你尝试使用的理由之一。Web技术发展是非常快速的。我很高兴地告诉你,CSS现在终于支持原生的变量了。
虽然CSS处理器还支持更多的特性,但是CSS添加原生的变量是很好的。这些举措使用Web更接近未来的技术。在这篇文章接下来的内容中,我将向你展示如何在CSS使用变量,以及如何使用它们让你的工作变得更轻松。
特别声明:为了能让CSS的原生变量与CSS处理器变量区分出来,我更喜欢将其称为CSS自定义属性。
你将学到
首先会向大家介绍CSS自定义属性的基本知识。我相信试图理解CSS自定义属性都必须从这一点开始。学习CSS自定义属性的基本知识是非常有意思的事情。其中更有趣的是,你可以在真实的项目中使用这些基本原理。
因此,我将通过三个简单的项目来向你展示CSS自定义属性的易用性。先来快速预览一下这三个项目:
项目1:使用CSS自定义属性创建组件变量
你可能在项目中已经使用到了组件变量。不管是React、Angular或者Vue中,使用CSS自定义属性都会使这个过程变得更简单。
项目2:使用CSS自定义属性实现皮肤切换
你可能在某个地方看到过类似的效果。在这个项目中将向你展示使用CSS自定义属性是如何简单的为Web网站实现皮肤切换的效果。
项目3: 创建CSS自定义属性展台(Booth)
这是最后一个项目。请不要介意这个名字。说实在的,我想不出一个更好的名字了。
注意如何动态更新容器的颜色,以及如何在input
(type="range"
)控制条的范围内更改外部容器的3D旋转效果。这个项目演示了使用JavaScript更新CSS自定义属性的便利性,以及即改即得的效果。你可以在上面示例中的input
框中输入任意颜色值或者拖动range
的进度条,浏览对应的效果变化。
为什么CSS自定义属性如此重要?
如果你是第一次接触CSS处理器中的变量或者CSS自定义属性,那么接下来介绍的几个方面将告诉你为什么CSS自定义属性对你而言是多么的重要。
理由1:代码更具可读性
不做过多的阐述,直接告诉你,使用CSS自定义属性可以让你的代码变得更具可读性和可维护性。
理由2:在大型项目中更易于修改
如果你将所有常量保存在一个单独的文件中,那么你想对一个变量进行更改时,不必跳过数千行代码。它变得很宽松,你可以把它放在任何一个地方。
理由3:更暴打发现输入错误
通过代码来查找错误是一件极为痛苦的事情。特别是你的错误是由于一个简单输入引起的,那就更令人恼火了。因为它是极难被人发现。CSS自定义属性的使用将消除这些麻烦。
为此,可读性和可维护性是CSS自定义属性最大的优势。
我们需要感谢 CSS自定义属性,让我们可以在CSS中使用原生的变量,而不再需要借助于类似Sass这样的CSS处理器。
定义CSS自定义属性
我们从一些熟悉的东西开始:JavaScript中的变量。
一个简单的JavaScript变量可以像下面这样来声明:
var
然后你可以给它赋值,比如:
amAwesome = "awesome string"
在CSS中,一个CSS自定义属性是以两个破折号(--
)开始的任何名称。
CSS自定义属性作用域
还有一件事你需要注意。
请记住,在JavaScript中,变量是有作用域一说。它们可能是全局作用域,也有可能是局部作用域。那么在CSS中,CSS自定义属性也有这样的说法。
比如下面这个示例:
:root {
--main-color: red }
:root
选择器可以选择到DOM元素中或document
树中最高顶级的元素。因此,在:root
选择器是声明的CSS自定义属性,其作用域的范围是全局范围,也就是全局作用域。
明白了?
示例
假设你想创建一个CSS自定义属性,该自定义属性存储Web网站皮肤的主色(Primary color)。那么你将会怎么做呢?
你将创建选择器范围。使用:root
创建一个global
自定义属性:
:root {}
然后声明自定义属性:
:root {
--primary-color: red;
}
请记住,CSS自定义属性可以是任何名称,但名称前必须要以两个破折号,比如--color
。
这是不是很简单。
使用CSS自定义属性
一旦声明了一个CSS自定义属性,并给其指定了一个值,那么你就可以在CSS的属性值中使用它。不过使用还是有点小问题。
如果你是在CSS处理器中使用,那么你必须在属性值中引用这个已声明的变量,例如:
$font-size: 20px;
.test {
font-size: $font-size;
}
但在CSS中使用已声明的CSS自定义属性,和在CSS处理器中使用声明的变量略有不同。你需要通过var()
函数来引用已声明的CSS自定义属性。
比如上面的示例,在CSS中使用已声明的CSS自定义属性,需要像下面这样使用:
一旦你理解了这一点,你就会开始喜欢CSS自定义属性,而且会很喜欢。
不过需要特别注意的是,CSS自定义属性不像Sass(或其他CSS处理器)中的变量,可以在许多地方使用变量,并且可以进行一些数学运算,但使用CSS自定义属性时,只能在CSS属性值中使用CSS自定义属性。
也不能像Sass这样的处理器一样直接做一些数学计算。如果你需要做一些数据计算,需要使用CSS的calc()
函数。我们将在后续的示例中会聊到这一点。
使用CSS自定义属性要做一些数学计算时,应该像下面这样通过calc()
函数来完成:
值得注意的地方
以下几个点是值得一提的地方。
自定义属性是普通属性,可以在任意元素上声明它们
CSS自定义属性可以在任意元素上,比如p
、section
、aside
、根元素,甚至是伪元素上声明。他们都会按照预期进行工作。
当你在对应的元素中调用相应的CSS自定义属性时,可以看到他们都能按照你的预期工作:
CSS自定义属性可以通过继承和级联规则来解决
比如下面这段示例代码:
和正常变量一样,--color
将会继承div
中的值:
CSS自定义属性可以通过@media和其他条件规则来实现
和其他属性一样,可以在@media
或其他条件规则中更改CSS自定义属性的值。比如下面这段示例代码,在较大的屏幕设备上将会改变gutter
的值:
CSS自定义属性可以用在HTML元素的style属性中
你可以选择在内联样式中声明CSS自定义属性,它同样是可以按照你的预期进行工作。
值得一提的是:CSS自定义属性是区分大小写的。这一点需要特别的注意。
解决多个声明的CSS自定义属性
CSS自定义属性和其他CSS属性一样,可以用标准级联解决多个相同声明的CSS自定义属性。比如下面这个示例:
就上面声明的CSS自定义属性,下列元素的文本颜色将会是什么颜色呢?
你能基于上述的知识点猜出来结果吗?
第一个<p>
元素是blue
。因为在p
选择器上没有显式定义--color
,所以它将继承来自:root
中的--color
的值。
:root { --color: blue; }
第一个<div>
元素将是green
。这一点非常的明显,因为在div
选择器上显示的声明了--color
自定义属性的值为green
:
div { --color: green; }
ID名为#alert
的div
元素,它的颜色不再是green
,将会是red
,那是因为在#alert
选择器中也显式的声明了--color
的值为red
:
#alert { --color: red; }
该ID选择器直接声明了一个自定义属性的作用域,其定义的值将会覆盖前面div
声明的自定义属性,那是因为#alert
选择器的权重高于div
选择器。由于最后一个<p>
元素在#alert
内,所以它的颜色也将是red
。在p
元素上没有显式声明CSS自定义属性,但在:root
元素上显式的声明了,因为正如你所期望的一样,其颜色为blue
。
:root { --color: blue; }
但CSS自定义属性和CSS中的其他属性是类似的,也具有继承这样的特性。所以最后一个<p>
元素会继承其父元素#alert
中声明的CSS自定义属性值:
#alert { --color: red; }
解决循环依赖
循环依赖的发生方式如下。
当一个变量依赖于它自己。也就是说,它使用的是引用自身的var()
。
另外一种方式就是两个或多个变量相互引用时:
目前唯一可破的方法是:不要在代码中创建具有循环依赖关系的CSS自定义属性。
无效的CSS自定义属性将会发生什么?
如果是语法错误,将会直接视为无效,但无效的var()
将会被该属性的初始值或继承值替代。比如下面的示例:
如你所预期的一样,--color
被var()
替换了,但是属性值background-color:20px
是一个无效值。由于background-color
不是一个可继承的属性,所以该值将默认为background-color
的初值值transparent
。
请注意,如果你已经写了background-color: 20px
没有自定认属性来替换,那么background-clor
将是无效的。要是前面有对应的声明,将会使用前面的声明。
在构建单个指令时要小心
当你设置如下所示的属性值时,20px
被解释为单个指令(Tokens)。
font-size: 20px
一个简单的方法是,20px
的值被视为一个单独的实体。在使用CSS自定义属性构建单个指令时,需要特别的小心。比如下面这段示例代码:
你可能已经预料到font-size
的值会产生20px
,但这是错误的使用方式。浏览器会将其解释为20 px
。注意20和px之间有一个空格
。因此,如果你必须创建单个指令,则用一个CSS自定义属性来表示整个指令,比如--size:20px
或者使用calc()
函数,比如calc(var(--size) * 1px)
(当--size
设置为20
时)。
如果你对这一点不怎么理解,没关系,在接下来的示例中会详细的解释这部分内容。
我们来动手做点东西
这是我们这篇文章中一直期待的一部分。将能过构建一些有用的项目来实践前面所讨论到的相关概念。还等什么呢?我们开始吧。
使用CSS自定义属性创建组件变量
假设我们要创建两个不同的按钮。这两个按钮的样式基本相同,只是略有一些细节不一样。
在这个示例中,只有background-color
和border-color
样式不同。如果你来做,你将会怎么做呢?这就是典型的解决方案。创建一个基类,比如说.btn
并添加变化的类。这个示例的结构如下:
<button class="btn">Hello</button>
<button class="btn red">Hello</button>
.btn
基类涵盖了按钮的基本样式。比如:
那么,对于略有差异的按钮效果怎么来实现呢?看下面的代码:
你知道我们是如何复制代码的吧。不过我们使用CSS自定义属性来做会做得更好。那么要做的第一步将是什么呢?
使用CSS自定义属性替换不同的颜色,但不要忘记为其添加默认值。
当你设置background:var(--color,black)
时,其意思就是background
的值是CSS自定义属性--color
的值。当--color
不存在时,就会使用其默认值black
。
这就是给CSS自定义属性设置默认值的方法。就像JavaScript或其他编程语言一样。这样使用是很有意义的。
对于有差异的按钮,你只需要像下面这样提供CSS自定义属性的新值就可以:
.btn.red {
--color: red }
这就是两个按钮所有的样式代码。现在,当你使用.red
类名时,浏览器会解析出不同的颜色值,并立即更新按钮的样式效果。
如果你花大量时间构建可重用的组件,像这样使用CSS自定义属性就会很方便。来看看他们之间对比的截图:
如果在你的组件库中,还有其他风格的按钮效果,使用CSS自定义属性,你就可以省去很多额外的时间:
使用CSS自定义属性切换Web站点的皮肤
切换Web网站的皮肤效果,我想你以前肯定有碰到过。比如像下面这样的效果:
那么这样的一个效果,使用CSS自定义属性能有多简单呢?我们来一起看看。
在此之前,我想先提一下,这个例子非常的重要。在这个示例中,将会涉及到使用JavaScript更新CSS自定义属性的一个方法。
我们真正想做的
CSS自定义属性美妙之处在于它们的响应性。一旦它们被更新,只要属性的值使用了CSS自定义属性,就将会被更新。
从概念上讲,下面这张图,能很好的解释整个过程。
因此,需要通过JavaScript给按钮添加点击事件的监听。
对于这个简单的示例,都是基于CSS自定义属性来对整个页面的背景颜色和文本颜色做切换。也就是说,当用户点击上面的任一按钮时,他们将CSS自定义属性设置为其他颜色。因此,页面的背景颜色和文本颜色会做相应的变化,从而实现切换皮肤的效果。
模板结构
页面所需的模板结果如下:
<div class="theme">
<button value="dark">dark</button>
<button value="calm">calm</button>
<button value="light">light</button>
</div>
<article>
...
</article>
在.theme
容器中包含了三个button
。另外为了节约篇幅,把article
元素中的内容省略了。
页面样式
这个项目的成功之处就是页面的样式。而其中的诀窍又很简单。我们没有直接设置background-color
和color
的值,而是基于CSS自定义属性来给他们设置属性值。下面就是我想要表达的意思:
body {
background-color: var(--bg, white);
color: var(--bg-text, black)
}
要实现切换皮肤的效果,原因很简单。每当单击一个按钮时,将要更改body
中对应的两个CSS自定义属性的值。在更改之后,页面的整体样式将被更新。是不是非常的简单。
接下来,我们看看怎么通过JavaScript来实现CSS自定义属性值的更新。
添加JavaScript代码
下面是这个示例效果所需的所有JavaScript代码:
const root = document.documentElement
const themeBtns = document.querySelectorAll('.theme > button')
themeBtns.forEach((btn) => {
btn.addEventListener('click', handleThemeUpdate)
})function handleThemeUpdate(e) {
switch(e.target.value) { case 'dark':
root.style.setProperty('--bg', 'black')
root.style.setProperty('--bg-text', 'white') break
case 'calm':
root.style.setProperty('--bg', '#B3E5FC')
root.style.setProperty('--bg-text', '#37474F') break
case 'light':
root.style.setProperty('--bg', 'white')
root.style.setProperty('--bg-text', 'black') break
}
}
看到上面这一坨JavaScript代码,千万别吓到你。事实上这比你想像的要简单得多。
首先,通过const root = document.documentElement
来获取根元素。这里的根元素指的就是<html>
元素。明白这一点是非常重要的。如果你感到好奇,这并奇怪,因为它需要设置CSS自定义属性的新值。同样的,可以通过const themeBtns = document.querySelectorAll('.theme > button')
选到.theme
元素下的所有button
元素。querySelectorAll
会生成一个类似数组的数据结构,需要对这个数组进行循环遍历。遍历每个按钮,并给其添加一个单击事件监听器。具体方法如下:
themeBtns.forEach((btn) => {
btn.addEventListener('click', handleThemeUpdate)
})
接下来我们解释一下handleThemeUpdate
函数。每个按钮补点击时都会有一个handleThemeUpdate
作为它的回调函数。注意,什么按钮被点击,然后执行正确的操作变得非常重要。在此基础上,使用了一个operator
开关器,并根据被单击的按钮的值执行一些操作。现在你再看一下JavaScript代码,你就能很轻易的理解它了。
构建CSS自定义属性展台
接下来我们将要构建一个CSS自定义属性展台:
记住,盒子的颜色是动态更新的,并且盒子的容器在三维空间中旋转,而且是随着range
输入的值做想应的角度旋转:
你可以在文章前面的示例中体验一下案例的效果。这是使用JavaScript更新CSS自定义属性的典型案例之一。
接下来,咱们一起看看这个案例是怎么实现的。
模板结构
以下是所需的组件:
- 一个
range
- 的输入框
- 存放指令的容器
- 包含其他输入框的容器,以及每个输入框所包含的输入字段
结构很简单:
有几个方面需要注意:
input type=range
- 的值的范围是
-50
- 到
50
- ,每次移动的值是
5
- ,其最小输入值为
-50
- 如果你不确定这个
range
- 输入是如何工作的,可以先查阅相关的文档
- 请注意如何使用
.color-boxes
- 和
.color-box
- 容器类名,在这些容器中存在
input
- 值得一提的是,第一个
input
- 的默认值是
red
了解了文档的结构之后,就可以这样做了:
- 将
.slider
- 和
.instructions
- 容器设置为绝对定位,让其脱离文档流
- 给
body
- 元素设置一个
background-color
- ,并且在其左下角添加一个花的背景图
- 将
.color-boxes
- 容器放置在页面的正中心
- 给
.color-boxes
- 容器添加样式
先做这些处理吧。
代码并不像你想像的那么复杂。我希望你能理解它。如果你不理解上面的代码,那么你可以通过下面的评论与我一起交流和讨论。
对于body
的效果需要更多的代码。由于我们使用background-color
和background-image
来给body
设置样式。所以使用background
的简写属性来设置多个背景可能是最好的一种选择。
body {
margin: 0;
color: rgba(255,255,255,0.9);
background: url('http://bit.ly/2FiPrRA') 0 100%/340px no-repeat, var(--primary-color);
font-family: 'Shadows Into Light Two', cursive;
}
url
指向的是向日葵图片的地址,接下来的一组属性0 100%
表示背景图像的位置。接下来的内容简单的介绍了CSS背景图像定位的基本原理。
/
后的另一部分表示background-size
的值。如果你把这个值缩小,那么背景图像也会相应的缩小。no-repeat
你可能知道其意思。它用来防止背景图像不重复铺满body
元素。最后,逗号后面的任何东西都是用来声明第二个背景的。在我们这个示例中,第二个背景只设置了一个背景颜色,而且其值是var(--primary-color)
。
你一看就知道,它就是一个CSS自定义属性。这也意味着你必须先声明这个CSS自定义属性。比如:
:root { --primary-color: rgba(241,196,15 ,1)}
--primary-color
的原值是yellow
。这没什么大不了的,接下来我们很快会在那里设置更多的CSS自定义属性。现在我们要做的是将.color-boxes
放到页面的正中间:
main.booth {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
main
容器是一个Flex容器,并正确地将其子元素设置到页面的正中心位置。另外再把.color-boxes
和他的子元素做得更好看一点。
.color-box {
padding: 1rem 3.5rem;
margin-bottom: 0.5rem;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 0.3rem;
box-shadow: 10px 10px 30px rgba(0,0,0,0.4);
}
设置了一个阴影效果。效果看上去有一些酷酷的感觉。咱们继续给.color-boxes
容器添加一些样式:
.color-boxes {
background: var(--secondary-color);
box-shadow: 10px 10px 30px rgba(0,0,0,0.4);
border-radius: 0.3rem;
transform: perspective(500px) rotateY( calc(var(--slider) * 1deg));
transition: transform 0.3s }
又是一坨代码,感觉好复杂的样子。事实上将其拆分一下,就会显得简单得多。
.color-boxes {
background: var(--secondary-color);
box-shadow: 10px 10px 30px rgba(0,0,0,0.4);
border-radius: 0.3rem;
}
你知道这是什么吗?在这里有一个新的CSS自定义属性,我们应该将其添加到:root
选择器中。
:root {
--primary-color: rgba(241,196,15 ,1);
--secondary-color: red;
}
第二种颜色是red
。这将给容器设置一个red
背景色。
接下来的代码可能会让部分同学感到困惑:
.color-boxes {
transform: perspective(500px) rotateY( calc(var(--slider) * 1deg));
transition: transform 0.3s }
暂时,我们可以简化上面的transform
属性的值。
例如:
transform: perspective(500px) rotateY( 30deg);
transform
使用了两个不同的函数。一个是perspective()
,另一个是rotateY()
。那么这两个函数有什么作用呢?perspective()
函数应用于3D空间中被变换的函数。它将激活三维空间,并在z
轴上赋予元素深度。
有关于perspective()这方面更多的资料,你可以点击这里或这里进行了解。
rotateY()
函数又是怎么一回事呢?通过perspective()
函数激活的三维空间中,元素有x
、y
、z
等平面,其中rotateY()
函数会让元素沿着y
平面旋转。
下图来自于codrops的图,能更好的帮助你理解这方面相关的知识。
有关于transform方面更多的介绍,可以点击这里进行了解。
我希望这些能帮助你消除一些压力。那我们回到开始的地方吧。
当你移动滑块时,你知道什么函数会影响.container-box
的旋转吗?它会被调用的rotateY()
函数,让这个盒子沿着Y
轴旋转。由于传入rotateY()
函数的值将是通过JavaScript来更新,因此它的值应该用一个CSS自定义属性来表示。
那么为什么用1deg
乘以这个CSS自定义属性呢?从经验上来说,建议构建单一的指令时,将值存储在没有带单位的CSS自定义属性中。然后通过calc()
函数将它们转换成任何你想要的单位。
当你有这些值的时候,你可以用这些值做任何你想做的事情。想要转换成deg
或vw
,你都可以任意选择。在这种情况下,通过将number
值乘以1deg
来把数字转换成带有单位的值。
由于CSS是不理解数学计算的,所以你必须通过calc()
函数来完成。一旦完成,我们就变得容易多了。这个变量的值可以在JavaScript中进行更新。
对于CSS来说,现在只剩下一点点了。
首先使用:nth-child
选择器来选择每个子元素。
这里你需要有一些远见。我们知道我们将更新每个盒子的背景颜色。我们还知道,这个背景颜色必须由一个CSS自定义属性来表示,所以它可以通过JavaScript来访问。对吧。
因此,我们可以这样做:
.color-box:nth-child(1) { background: var(--bg-1)}
非常容易的一件事。不过有一个问题。如果这个变量不存在,又会发生什么呢?根据前面介绍的内容,针对这一问题,我们需要做一个降级处理。就是添加一个默认颜色,比如像下面这样:
.color-box:nth-child(1) { background: var(--bg-1, red)}
在这个特殊的案例中,我选择不提供任何的降级处理。
如果在属性值中使用CSS自定义属性无效,则该属性将接受其初始值。这一点前面已经提到过了。因此,当--bg-1
无效或不可用时,background
将默认为其初始值transparent
.初始值是指当属性没有显式设置时的值。例如,如果不设置元素的背景颜色,它将默认为transparent
。初始值是一种默认的属性值。
加入一些JavaScript代码
对于JavaScript代码,我们要做的事情会很少。
首先让我们来处理滑块。仅只需要五行代码就可以了。
const root = document.documentElementconst range = document.querySelector('.booth-slider')
range.addEventListener('input', handleSlider)function handleSlider (e) {
let value = e.target.value
root.style.setProperty('--slider', value)
}
这对你而言是不是很简单,对吧。
首先,通过const range = document.querySelector('.booth-slider')
选到了滑块元素。然后使用range.addEventListener('input', handleSlider)
给input
元素添加一个监听事件,当input
的范围值得到改变时,将会调用handleSlider
回调函数。接下来做的就是编写handleSlider
回调函数:
function handleSlider (e) {
let value = e.target.value
root.style.setProperty('--slider', value)
}
root.style.setProperty('--slider', value)
的意思是获取根元素的style
样式,然后设置其属性值。
处理颜色变化
就像处理滑块值的变化一样简单。
const inputs = document.querySelectorAll('.color-box > input')
inputs.forEach(input => {
input.addEventListener('input', handleInputChange)
})function handleInputChange (e) {
let value = e.target.value let inputId = e.target.parentNode.id
let inputBg = `--bg-${inputId}`
root.style.setProperty(inputBg, value)
}
通过const inputs = document.querySelectorAll('.color-box > input')
获取所有input
元素。然后给所有input
设置一个事件监听器:
inputs.forEach(input => {
input.addEventListener('input', handleInputChange)
})
写handleInputChange
回调函数:
function handleInputChange (e) {
let value = e.target.value let inputId = e.target.parentNode.id
let inputBg = `--bg-${inputId}`
root.style.setProperty(inputBg, value)
}
唷,唷,唷.......就是这么的简单。全部搞定!
还错过了什么
当我注意到我在任何地方都没有提到浏览器对CSS自定义属性的支持时,我已经完成了这篇文章的初稿。现在也该来收拾一下相应的残局了。
浏览器对CSS自定义属性的支持并不坏。可以说相当的不错,到目前为止所有的现代浏览器中都对CSS自定义属性做了良好的支持。在写这篇文章的时候,已经超过87%了。
那么问题来了,我现在可以在项目中使用CSS自定义属性了?我的回答是的!不过,一定要检查一下你自己的用户群体。
值得庆幸的是,你可以使用Myth这样的处理器。它将把一些CSS未来的特性做了相应的处理。言外之意,还未支持的CSS特性,你现在就可以使用,听起来是不是很爽。难道不是吗?
如果你有使用PostCSS的经验,那么这同样是使用未来CSS的一个很好的方法。这里有一个CSS自定义属性的PostCSS模块。
如果你从未接触过PostCSS相关的东西,那么你有必要花点时间去了解或者学习一下了。
那今天就到这里吧。这就是我要和大家聊的东西,有关于CSS自定义属性,应该掌握的一些知识点。