跟我一起全面了解一下CSS变量

跟我一起全面了解一下CSS变量

什么是css变量?

CSS变量,即CSS variable。官方的名称是 级联变量的CSS自定义属性 ,即 CSS custom properties for cascading variables 。类似于sass,less等预处理器的变量。css变量同样具备声明,引用,以及作用域等变量特性。

如果你学过其他编程语言,应该很熟悉变量的概念。变量用于存储和更新你的程序所需要的值,以便使它运行。在一些命令式编程语言中,像Java、C++亦或是JavaScript,通过变量我们能够跟踪某些状态。变量是一种符号,关联着一个特定的值,变量的值能随着时间的推移而改变。不同于其它编程语言,在像CSS这种声明式语言中,随着时间而改变的值并不存在,也就没有所谓变量的概念了。

CSS 引入了一种层级变量的概念,从而能够从容应对可维护性的挑战。这就会使得在整个 CSS tree 中都可以象征性的引用一个变量。

CSS变量能解决什么问题?

在构建大型站点时,开发者通常会面对可维护性的挑战。在这些网页中, 所使用的 CSS 的数量是非常庞大的,并且在许多场合大量的信息会重复使用。例如,在网页中维护一个配色方案,意味着一些颜色在CSS文件中多次出现,并被重复使用。当你修改配色方案时,不论是调整某个颜色或完全修改整个配色,都会成为一个复杂的问题,不容出错,而单纯查找替换是远远不够的。

当然,目前解决维护大型站点css文档带所来的困扰,是有非常成熟的解决方案的。比如,可以采用sass,less,stylus等预处理器。

但是,CSS变量有着天然优势。不同于预处理器,必须最终编译成普通的css代码,CSS变量可以被浏览器识别解析,甚至可以用javascript访问或重新赋值。比如,在线修改一个网站主题色,只需修改相应变量即可实现。某种意义上,这为css开启了一扇通向各种可能性的大门。

除此之外,w3c规范是这样描述CSS变量的

使用CSS变量,给看似随机的值加上富有信息的名字,从而使得大文件更容易阅读和编辑,更少出错。因为,你只需要在自定义属性中改变一次值,所有应用了这个变量的地方都会自动跟着一起改变。

可以自定义变量名称,使变量更有语义,更有可读性。毕竟primary-color比#0055aa更容易理解,特别是同样的颜色出现在不同的文件中的时候,颜色之间的关联关系更加直观。

总结:

1,提高大型系统文档的可维护性;

2,提高系统代码的可读性;

3,可以在浏览器中实时修改生效;

CSS变量的声明

CSS变量也叫CSS自定义属。即为属性,同样是由合法的标识和合法的值组成。不同于普通属性,自定义属性有"--"前缀,早期的标准也有采用"var-"作为前缀。之所以会采用"--",是因为通常变量声明常用的前缀"$"或者"@"已经被预处理器sass和Less占用了。为了保证css变量和预处理器变量不产生冲突,所以采用了"--"。

看下面例子

声明变量

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

引用变量,

.primary-button{
    background: var(--primary-color)
}


  • 变量声明中,属性值不能直接进行数学运算,需要借助calc()函数

例如

--font-size:12px *2; /**错误的方式**/
--font-size:calc(12px*2);/**正确的方式**/


  • CSS变量可以用在任意css选择器中,甚至可以用在style以及@media中

各种选择器中使用

:root{
    --primary-color:green;
div{
    --normal-size:14px;
#con{
    --local-color:blue;
    color:var(--primary-color);
#con .box{
    --content:'normal box';
    color:var(--local-color)
.box:after{
    content:var(--content);
}

HTML的style中使用

<body style="--color:red;">
    <span style="color:var(--color)">CSS变量</span>
</div>

@media查询中使用

:root{
    --column-size:4;
@media screen and (min-width: 768px) {
    :root{
        --column-size: 3;
}


  • CSS变量不同于预处理器变量,css变量只能用于属性值

例如,sass中我们可以这样声明:

$name:color;
$primary-color:green;
$i:1;
.box{
    #{$name}:$primary-color;
.child-#{$i}{
    color:$primary-color;
}

但是,换成CSS变量,下面这种用法会抛出不合法的属性名异常

:root{
   --name:color;
   --primary-color:green;
.box{
    var(--name):var(--primary-color);
}


  • CSS变量名称区分大小写,--primary-color 不同于 --Primary-Color
:root{
    --primary-color:red;
    --Primary-Color:green;
.one{
    color:var(--primary-color); /**red**/
.two{
    color:var(--Primary-Color); /**green**/
}
  • 属性名可以包含数字,字母,以及下划线或者短横线,也可以是中文,日文或者韩文
body {
    --深蓝: #369;
    background-color: var(--深蓝);
}

注意,属性名不能包含$,[,^,(,%等字符

变量的引用

CSS变量的引用,需要用到var( <custom-property-name> , <declaration-value> ? )函数,

css其实有很多有用的函数, 这里

var函数的第一个参数是要替换的自定义属性的名称。函数的可选第二个参数用作备用值。如果第一个参数引用的自定义属性无效,则该函数将使用第二个值。

例如

:root{
     --bg-color: green;
.box{
    background-color:var(--bg-color, red);
}
  • 备用值declaration-value中允许使用逗号,var()函数第一个逗号以后的部分作为整体返回,用作var()的备用值。

例如

.box{
    background-image:var(--bg-img, url(./one.png), url(./two.png));
}

上面例子,--bg-img变量不存在,所以计算结果是

background-image: url(./one.png), url(./two.png)


  • var()函数只解析第一个参数引用的变量

看下面例子,box的字体颜色是什么呢?

body{
    color:red;
:root{
    --two-color:blue;
.box{
    color: var(--one-color, --two-color);
}

答案是red。因为var函数只会解析第一个参数,剩余的参数会整体返回作为备用值,并且不会解析。因为--two-color的值是一个无效的颜色值。所以最终的颜色是从body继承的red。备用值如果是变量,同样需要var()函数去引。这意味着var函数是可以嵌套的。

正确的书写方式

.box{
    color: var(--one-color, var(--two-color));
}

当然--two-color同样可以有自己的备用值

.box{
    color: var(--one-color, var(--two-color, #0055aa));
}

由上例可知,var()函数可以用在var()的参数当中。

另外,需要注意的是,备用值可以包含任何字符,但是部分有特殊含义的字符除外,例如换行符、不匹配的右括号(如 )、]或} )、感叹号以及顶层分号(不被任何非 var () 的括号包裹的分号,例如 var(--bg-color, --bs ; color) 是不合法的,而 var(--bg-color, --value ( bs;color ) ) 是合法的)。

例如,下面的例子空格作为分隔符是合法的

:root{
    --border-one:1px;
.box{
    border-width: var(--border-two, 1px 2px 3px 4px);
}


  • CSS变量在CSS变量声明中引用

例如

:root{
    --primary-color:green;
    --button-default-color:var(--primary-color);
    --normal-font-size:16px;
    --button-normal-font-size:var(--normal-font-size);
}

注意:尽量避免循环引用


  • CSS变量在拼接计算中引用

如果变量都是字符串,可以直接拼接

:root{
    --text-one:'hello';
    --text-two:'world'
.box:after{
    content: var(--text-one)' 'var(--text-two);
}

变量和单位不能直接拼接

:root{
    --size-one:14;
.box{
    font-size:var(--size-one)px; /**无效**/
}

计算结果将是

font-size: 14 px;

由上可知,变量和单位直接拼接会多出一个空格,正确的拼接方式是借用calc()函数,进行计算

font-size:calc(var(--size-one)*1px);

当然,calc也可以用作CSS变量值当中

--size-two:calc(20px * 2)

变量值的类型

变量值可以是有效的css属性值,比如颜色,尺寸,定位,角度,字符串,数字等,也可以是多个字段的组合,甚至可以是javascript语句

例如

:root{
  --main-color: #4d4e53;
  --main-bg: rgb(255, 255, 255);
  --logo-border-color: rebeccapurple;
  --header-height: 68px;
  --content-padding: 10px 20px;
  --base-line-height: 1.428571429;
  --transition-duration: .35s;
  --external-link: "external link";
  --margin-top: calc(2vh + 20px);
  /* Valid CSS custom properties can be reused later in, say, JavaScript. */
  --foo: if(x > 5) this.width = 10;
}

上面代码中,--foo的值在 CSS 里面是无效值,但是可以被 JavaScript 读取。这意味着,可以把样式设置写在 CSS 变量中,让 JavaScript 读取。

这里有一个比较重要的概念需要理解和区分。在传统的CSS概念里,属性的有效性(validity)对于自定义属性来讲,并不适用。当自定义属性被解析,浏览器不知道哪里为调用到它们,所以几乎所有的值都是有效的。

不幸的是,这些有效值能通过var()函数操作符来调用,即使在当前上文没有意义。属性和自定义的值会导致无效的CSS声明,所以有了 计算时有效(valid at computed time) 的概念。

CSS变量的作用域和继承

  • CSS自定义属性可以被继承,并遵循CSS级联规则

CSS变量,即CSS自定义属性。跟普通属性一样,CSS自定义属性的继承,优先级同样遵循 CSS级联规则 。一个元素上没有定义自定义属性,该自定义属性的值会继承其父元素。按照层叠关系向上查找。最终查找到根元素,也即前面例子中用到的:root选择器所定义的变量。因此,变量的作用域就是它所在的选择器的有效范围

:root{
    --primary-color:green;
body{
    --primary-color:blue;
.box{
    --primary-color:red;
.box__txt{
    color:var(--primary-color)
}


html代码

<!DOCTYPE html>
<html lang="en">
    <div class="box">
        <span class="box__txt">CSS变量</span>
    </div>
</body>
</html>

:root中声明的--primary-color,作用域是整个dom树;body中声明的--primary-color,作用域是整个body范围内有效;.box中声明的--primary-color,作用域是其本身及其后代选择器。.box__text容器中并没有声明变量--primary-color,但是会继承父元素中声明的--primary-color。所以最终.box__text容器的字体颜色最终呈现为red。由此,CSS变量的作用域与其选择器有效范围一致。

  • 自定义属性优先级支持!important
:root{
    --primary-color:green;
body{
    --primary-color:blue;
.box{
    --primary-color:red!important;
    font-size:36px;
.box-1{
    --primary-color:orange;
    font-size:24px;
.box__txt{
    color:var(--primary-color)
}

html代码

<!DOCTYPE html>
<html lang="en">
    <div class="box box-1">
        <span class="box__txt">CSS变量</span>
    </div>
</body>
</html>

依据就近原则,字体大小呈现为24px;依据!important关键字的优先级,字体颜色呈现为red。

思考一下面例子,.box__txt容器字体将是什么颜色?

:root{
    --primary-color:green;
    color:#333333;
body{
    --primary-color:blue;
    color:hotpink;
.box{
    --primary-color:red;
    font-size:36px;
.box-1{
    --primary-color:inherit;
    font-size:24px;
.box__txt{
    color:var(--primary-color)
}


CSS变量的兼容性

除了IE11(它不支持CSS变量),所有主流浏览器都对CSS变量有全面地支持。见 这里

可以使用 @support 命令进行检测是否支持CSS变量

@supports ( (--a: 0)) {
  /** supported **/
@supports ( not (--a: 0)) {
  /* not supported */
}

对于不支持CSS变量的浏览器,一个变通的方案是使用具有虚拟查询条件(dummy conditional query)的 @supports 代码块

section {
    color: gray;
@supports(--css: variables) {
    section {
        --my-color: blue;
        color: var(--my-color, 'blue');
}


javaScript 也可以检测浏览器是否支持 CSS 变量

const isSupported =
  window.CSS &&
  window.CSS.supports &&
  window.CSS.supports('--a', 0);
if (isSupported) {
  /* supported */
} else {
  /* not supported */

javascript 操作CSS变量

  • 可以直接通过JavaScript代码访问CSS变量

getPropertyValue(): 读取变量

root.style.getPropertyValue('--global-color').trim();

注意,上面代码,仅仅读取root.style中声明的变量,并不能读取:root选择器中声明的变量

读取选择器中的变量,需要借助styleSheet或者getComputedStyle

const root = document.documentElement
let _style = getComputedStyle(root)
_style.getPropertyValue('--global-color').trim()

setProperty():设置变量

root.style.setProperty('--global-color','green');

removeProperty(): 删除变量

root.style.removeProperty('--global-color');

上面代码中,setProperty和removeProperty,只是对root.style进行操作,并不能:root选择器起作用。那么是否可以借助getComputedStyle进行操作呢?

const root = document.documentElement
let _style = getComputedStyle(root)
_style.setProperty('--global-color','blue')
_style.removeProperty('--global-color')

执行上面代码,浏览器将会报错:

Uncaught DOMException: Failed to execute 'setProperty' on 'CSSStyleDeclaration':
 These styles are computed, and therefore the '--global-color' property is read-only.

因为,getComputedStyle的返回值上的属性是只读的

此外,可以通过操作styleSheet,insertRule方法,重写覆盖变量值。

var style = document.createElement("style");
style.type = 'text/css';
style.appendChild(document.createTextNode(""));
document.head.appendChild(style);
sheet = style.sheet || style.styleSheet;
sheet.inserRule(`:root{
    --global-color: orange;
}`,0)

缺点很明显,操作比较繁琐,不适用于维护较大的项目;

下面这种方式更适合大型项目

:root.global__style--primary{
    --button-color: green;
    --button-font-size:14px;
    --button-text-color: #fffff;
:root.global__style--warning{
    --button-color:orange;
    --button-font-size:12px;
    --button-text-color:#fffff;
.button{
    background-color:var(--button-color);
    font-size:var(--button-font-size);
    color:var(--button-text-color);
}

通过操作root的className,更新变量

let root = document.documentElement;
root.className = 'global__style--primary'
// or
root.className = 'global__style--warning'

利用CSS变量做一些有趣的事情

  • 实现的简单鼠标跟随

JS code

let root = document.documentElement;
root.addEventListener("mousemove", e => {
  root.style.setProperty('--mouse-x', e.clientX + "px");
  root.style.setProperty('--mouse-y', e.clientY + "px");

css code

.mover {
  width: 50px;
  height: 50px;
  background: red;
  position: absolute;
  left: var(--mouse-x);
  top: var(--mouse-y);
}

html code

<div class="mover"></div>

查看效果, 这里

  • 根据背景色,设置合适的文字颜色

首先看一下,通过javascript方式实现字体颜色计算的公式

var rgb = [255, 0, 0];
function setForegroundColor() {
  var sum = Math.round((
             (parseInt(rgb[0]) * 299) 
           + (parseInt(rgb[1]) * 587) 
           + (parseInt(rgb[2]) * 114)) / 1000);
  return (sum > 128) ? 'black' : 'white';

原理比较简单,根据RGB值,计算一个值,判断是否大于128,来决定使用black or white

CSS变量如何实现呢?看下面

:root {
  --red: 28;
  --green: 150;
  --blue: 130;
  --accessible-color: calc(
        (var(--red) * 299) +
        (var(--green) * 587) +
        (var(--blue) * 114) /
      ) - 128
    ) * -1000
.button {
  color:
    rgb(
      var(--accessible-color),
      var(--accessible-color),
      var(--accessible-color)
  background-color:
    rgb(
      var(--red),
      var(--green),
      var(--blue)
}

查看效果, 这里

  • CSS变量条件语句

使用二元条件的计算

:root {
    --is-big: 0;
.is-big {
    --is-big: 1;
.block {
    padding: calc(
        25px * var(--is-big) +
        10px * (1 - var(--is-big))
    border-width: calc(
        3px * var(--is-big) +
        1px * (1 - var(--is-big))
}

在这个例子中,我们给所有带有.block的元素都加上了10px的内边距和1px的边框,除非这些元素上的--is-big变量不为1,在这种情况下,它们将分别变为25px和3px. 理解这一点的机制非常简单:我们在使用calc()的单个计算中使用我们的可能值,我们使其中一个值无效,并且基于变量的值得到另一个值,该变量的值可以是1或0。换句话说,我们有25px * 1 + 10px * 0和25px * 0 + 10px * 1两种情况。

更复杂的条件

我们不仅可以使用这个方法在两个可能的值中选择,还可以从3个或者更多的值中选择。然而,对于每增加一个可能的值,计算将变得更加复杂。为了3个可能的值中进行选择,计算将看起来像这样:

.block {
    padding: calc(
        100px * (1 - var(--foo)) * (2 - var(--foo)) * 0.5 +
         20px * var(--foo) * (2 - var(--foo)) +
          3px * var(--foo) * (1 - var(--foo)) * -0.5
}

原理是一样的:我们只需要将每个可能的值乘以一个表达式,当该值的条件是我们需要的值时,该值等于 1 ,在其他情况下为 0 。这个表达式可以很容易的组成:我们只需要使我们的条件变量的每个其他可能的值无效,这样做后,我们需要在那里添加触发值,看看是否需要调整结果,使其等于1。

  • 画一个百分比饼图
.pie {
    --p: 25;
    width: 300px;
    height: 300px;
    border-radius: 50%;
    background: conic-gradient(#ab3e5b calc(var(--p)*1%), #ef746f 0%);
    transition: --p .5s;
    position: relative;
.pie:after {
    position: absolute;
    z-index: 1;
    top: 20%;
    left: 0;
    width: 100%;
    text-align: center;
    color: #fff;
    counter-reset: p var(--p);
    content: counter(p) '%';
}

html code

div id="pie" class="pie" style="--p:25"></div>
<button id="increment">add 10</button>

js code

window.addEventListener('DOMContentLoaded', function () {
    var p = 25
    var pie = document.getElementById('pie')
    pie.style.setProperty('--p', p)