进阶使用CSS自定义属性
在之前一篇介绍CSS自定义属性的文章中,我们介绍了什么是CSS自定义属性,
var()
、calc()
。本篇文章中,为了进一步使用它,我们将介绍CSS自定义属性的其他用法。
自定义原则
在传统的CSS中,通常我们需要写重复的属性值,而自定义原则能让我们避免这种情况。做到“一处定义,处处使用”。
:root { --theme-color: gray; } .button { background-color: var(--theme-color); } .title { color: var(--theme-color); }
以后,一旦要调整,只需要改动--theme-color
这个属性的值。
进一步发挥calc()
计算
我们知道,calc()与自定义属性结合能实现属性值的计算。
现在,有这样一个场景:实现一个3列的网格布局,其中:内边距8px,网格中的box外边距为8px。
原有的代码:
.image-grid {padding: 8px;} .image-grid > .box {margin: 8px;}
下面使用calc()
改进:
:root { --image-grid-spacing: 16px; } .image-grid { padding: calc(var(--image-grid-spacing)/2); } .image-grid > .box { margin: calc(var(--image-grid-spacing)/2); }
注意: Safari/WebKit 目前在 calc()中的计算还有一些问题,这些问题在 Safari 10.1 中有望得到解决。
再来看一个例子:用flexbox实现响应式网格。
.image-grid { display: flex; flex-wrap: wrap; padding: 8px; } .image-grid > .image { margin: 8px; width: calc(100% - 16px); } @media (min-size: 600px) { /* 3 images per line */ .image-grid > .image { width: calc(100% / 3 - 16px); } } @media (min-size: 1024px) { /* 6 images per line */ .image-grid > .image { width: calc(100% / 6 - 16px); } }
上面这段代码实现的是一组响应式布局,但乍看上去,一头雾水。默认情况下,图片排成一列,也就是一行只显示一张图片;如果屏幕尺寸是600px、1024px…相应的,图片排列变成了三列或者是六列。和上一段代码例子中一样,此处容器边缘宽度和网格间距都是16px。
calc() 中的计算内容比较复杂,我们需要加上注释解释。但如果用上了自定义变量,情况就不同了:
:root { --grid-spacing: 16px; --grid-columns: 1; } .image-grid { display: flex; flex-wrap: wrap; padding: calc(var(--grid-spacing) / 2); } .image-grid > .image { margin: calc(var(--grid-spacing) / 2); width: calc(100% / var(--grid-columns) - var(--grid-spacing)); } @media (min-size: 600px) { :root { --grid-columns: 3; } } @media (min-size: 1024px) { :root { --grid-columns: 6; } }
可以看出,所有的计算都是在一处完成的。在媒体查询中需要改变的只有自定义属性的值。
CSS 与 Javascript之间的桥梁:自定义属性
假设现在有一个容器元素,我们希望当用户点击它的时候可以移动到最后一位。如果在该容器中设置一个辅助性元素,我们可以这样移动它:
.container { position: relative; --clickX: 0; --clickY: 0; } .container > .auxElement { position: absolute; transform: translate(var(--clickX, 0), var(--clickY, 0)); }
const container = document.querySelector('.container'); container.addEventListener('click', evt => { container.style.setProperty('--clickX', `${evt.clientX}px`); container.style.setProperty('--clickY', `${evt.clientY}px`); });
上面,我们使用 CSS 处理视觉表现上了,不再需要通过 JS 更改内联样式
。
一次定义,处处使用
逻辑上的变化可能伴随着大面积视觉表现上的更改,典型的例子就是选择主题,更换主题时可能引起大部分元素视觉上的变化。
以音乐播放器为例,如果你希望界面颜色随着当前收听专辑的更改而变化,从前你需要维护一系列会出现颜色变化的元素以及属性,需要的时候依次更改:
const thingsToUpdate = new Map([ ['playButton', 'background-color'], ['title': 'color'], ['progress': 'background-color'] ]); for (let [id, property] of thingsToUpdate) { document.getElementById(id).style.setProperty(property, newColor); }
HTML 结构如下:
<span class="title js-update-color">Song title</span> <button class="play-button js-update-background">Play</button> <div class="progress-track js-update-background"></div>
const colorList = document.querySelectorAll('.js-update-color'); for (let el of colorList) { el.style.setProperty('color', newColor); } const backgroundList = document.querySelectorAll('.js-update-background'); for (let el of backgroundList) { el.style.setProperty('background-color', newColor); }
这种方式,不管怎么样,都要常常更新跟随主题变化的元素和属性,所以这个方法会让后续维护变得艰难。
还有一种解决方式是引入一个新样式(!important),它将会覆盖旧样式。这个方法相对好一些(虽然比较 hacky),但还是避免不了要覆盖一系列的样式,这其中依然有着维护成本。
- 通过自定义属性处理
只要改变位于 DOM 结构中最高点的元素,接着让浏览器去改变该节点之下的节点:
.player { --theme-color: red; } .play-button { background-color: var(--theme-color); } .title { color: var(--theme-color); } .progress-track { background-color: var(--theme-color); }
document.querySelector('.player').style.setProperty('--theme-color', newColor);
JavaScript 根本不需要知道哪些元素哪些属性会发生变化,也不需要开发者维护受影响的元素列表。使用自定义元素,明显比前文中的方案都好!
这样,CSS和JS分别独立实现样式和逻辑部分,维护起来更加容易。