你的网站可以一键变色吗?

3,690 阅读7分钟
原文链接: zhuanlan.zhihu.com

若干年前写过一个叫「网站换色精灵」的小工具,原理是调整网站所有图片的色相、饱和度和亮度。然而并没有什么人用……或许是因为做得不好,又或许这本身就是一种伪需求。

得益于 Web 标准的发展和设计风格的变化,前端开发者从通过切图还原设计逐渐变为通过代码还原设计。CSS 预处理器也在一定程度上弥补了 CSS 本身表达能力的不足,许多 UI 框架(比如 Element)将基础的颜色值作为配置项供使用者定制,其余的颜色则在它们的基础上调整亮度/饱和度,或者与其他颜色混合而成。虽说做不到一键变色,但是通过重新构建来改变整个网站的配色是没有问题的。

除了可定制,这样做还可以让代码变得更容易维护。相比较充斥着各种颜色值的 CSS 代码,甚至可以表达出一些配色思路(或者让不善于设计的我写出至少配色上还过得去的 UI)。

配色思路?

下面这段样式是从七牛的管理控制台中摘抄的:

.btn.btn-primary {
    color: #1989fa;
    background-color: rgba(25,137,250,.04);
    border-color: rgba(25,137,250,.4)
}

心算一下(Shift + 左击),就能发现上面的三个颜色是同一种颜色,只是透明度不同。


由于页面的背景是纯白的,因此调整颜色透明度可以看成是在调整颜色的亮度。按钮虽然只用了「一种颜色」,但是看起来还是比较和谐的。

从中可以看出对主按钮常规状态的设计思路是:

  1. 使用 #1989fa 作为基础颜色;
  2. 文字颜色使用基础颜色;
  3. 将基础颜色调亮 96% 作为背景色;
  4. 将基础颜色调亮 60% 作为边框的颜色。

预处理器?

使用像 Sass 这样的预处理器很容易实现上面的需求:

$primary-color: #1989fa;

$text-color: $primary-color;
$background-color: scale-color($primary-color, $lightness: 96%);
$border-color: scale-color($primary-color, $lightness: 60%);

.btn-primary {
  color: $text-color;
  background-color: $background-color;
  border-color: $border-color;
}

Sass 会在编译期间计算出表达式的值,生成这样的 CSS 代码:

.btn-primary {
  color: #1989fa;
  background-color: #f6faff;
  border-color: #a3d0fd;
}

不过,使用预编译器就意味着需要构建——总有一些人不喜欢「构建」过程,或者倾向于使用更「原生」的解决方案。

那么,使用纯 CSS 可以在一定程度上实现这样的效果吗?答案是肯定的,七牛管理控制台的例子中就用了透明度来实现提升亮度的效果。问题在于,其中的颜色值出现了多次,可维护性还是不高。

CSS 变量

CSS 变量是一项实验中的技术,不过现代浏览器大多都已经支持了,所以如果你的网站面向的用户使用的基本都是现代浏览器,可以考虑使用这项技术。后文尝试使用这项技术来描述 UI 的配色,编写更容易维护的纯 CSS。

我不打算详细介绍 CSS 变量,如有兴趣可以查阅 MDN 和相关规范。不过不必担心,即便对 CSS 变量了解不多也没关系,后文在用到 CSS 变量时会有一些简单的解释。

我打算写一个页面作为例子。

配色

好吧,作为一个不会设计的前端工程师,我准备找一个现成的颜色主题。在 Adobe Color CC 上最受欢迎的颜色主题里挑了个顺眼的,就可以开始配色了。有了颜色主题,配色会容易一些,只需要选 3 ~ 4 种颜色,就可以配出一个不错的 UI 了。

背景色和文字颜色

为了确保可读性,只要选出反差和亮度差最大的两种颜色即可。在这个颜色主题里,自然是前两个偏黑白的 #323a40 和 #e5eef4 了。我想做一个暗色的配色,因此选择前者为背景色,后者为文字颜色。

:root {
  --background-color: #323a40;
  --text-color: #e5eef4;
}

CSS 变量以两个连字符开头,定义 CSS 变量与设置属性类似。上面这段代码定义了 --background-color 和 --text-color 这两个 CSS 变量。:root 选择器会选择根节点(也就是 <html>),与 html 的区别在于优先级更高,适合用于定义全局 CSS 变量。

html {
  background: var(--background-color);
  color: var(--text-color);
}

要引用定义的 CSS 变量也很简单,只需要使用 var 函数即可。这样,页面的背景色和文字颜色就设置好了。

在 JSFiddle 上 DIY

主色

然后,选择一个主色。主色通常被用在超链接、主按钮、logo 上。为了它们更突出,应该选择一个与背景色和文字颜色都有一定反差的颜色。这里,我选择颜色主题中的第三个颜色 #37b0c0。

:root {
  --background-color: #323a40;
  --text-color: #e5eef4;
  --primary-color: #37b0c0;

  --input-size: 30px;
  --input-padding-horizontal: 10px;
  --button-border-radius: 4px;
}

button {
  background: none;
  border: 1px solid var(--primary-color);
  border-radius: var(--button-border-radius);
  color: var(--primary-color);
  height: var(--input-size);
  padding-left: var(--input-padding-horizontal);
  padding-right: var(--input-padding-horizontal);
  transition: all .15s ease;
}

button:hover {
  background: var(--primary-color);
  color: var(--background-color);
  cursor: pointer;
}

CSS 变量不仅可以定义颜色值,上面的代码还用 CSS 变量定义了按钮的大小、内边距和边框的半径。

在 JSFiddle 上 DIY

透明度

CSS 里并没有像 Sass 里 darken、lighten 那样的颜色函数,可以考虑使用透明度在一定程度上实现加深或者减淡的效果。不幸的是,CSS 里同样也没有操作颜色透明度的函数。我们只能把颜色的三个分量拆开定义:

:root {
  --background-color-r: 51;
  --background-color-g: 59;
  --background-color-b: 64;
  --background-color: rgb(
    var(--background-color-r),
    var(--background-color-g),
    var(--background-color-b)
  );

  --text-color-r: 229;
  --text-color-g: 238;
  --text-color-b: 244;
  --text-color: rgb(
    var(--text-color-r),
    var(--text-color-g),
    var(--text-color-b)
  );

  --primary-color-r: 62;
  --primary-color-g: 176;
  --primary-color-b: 190;
  --primary-color: rgb(
    var(--primary-color-r),
    var(--primary-color-g),
    var(--primary-color-b)
  );

  --input-size: 30px;
  --input-padding-horizontal: 10px;
  --button-border-radius: 4px;
}

是的,这么定义很麻烦。不过,每个颜色值还是只会出现一次。

input, button {
  --border-color: rgba(
    var(--text-color-r),
    var(--text-color-g),
    var(--text-color-b),
    var(--border-color-alpha, .3)
  );
  border: 1px solid var(--border-color);
}

其中,var(--border-color-alpha, .3) 表示引用 --border-color-alpha 变量的值,如果变量没有定义或者无效,则回退到 .3。这样一来,input 和 button 的边框颜色会变成背景色混合 30% 的文本颜色。

input:focus {
  --border-color-alpha: .6;
}

当焦点在 input 上时,--border-color-alpha 的值将变为 .6,此时边框颜色会变成背景色混合 60% 的文本颜色。

我使用同样的方法写了一个友好的 header。

在 JSFiddle 上 DIY

白天主题

产品经理找到我说,大多数程序员都觉得我做的页面很友好,但是少数非夜猫子程序员觉得这个主题在白天太刺眼了,希望能有一个「白天主题」。

好在 JavaScript 可以设置 CSS 变量的值,而白天主题只需要把背景颜色和文字颜色互换就可以了。

const themes = [
  {
    name: 'dark',
    scheme: {
      '--background-color-r': 51,
      '--background-color-g': 59,
      '--background-color-b': 64, 
      '--text-color-r': 229,
      '--text-color-g': 238,
      '--text-color-b': 244
    }
  },
  {
    name: 'light',
    scheme: {
      '--background-color-r': 229,
      '--background-color-g': 238,
      '--background-color-b': 244, 
      '--text-color-r': 51,
      '--text-color-g': 59,
      '--text-color-b': 64
    }
  }
];

let currentTheme = 0;
window.nextTheme = function () {
  currentTheme = (currentTheme + 1) % themes.length;
  const theme = themes[currentTheme];
  Object.keys(theme.scheme).forEach(name => {
    const value = theme.scheme[name];
    document.documentElement.style.setProperty(name, value);
  });
}

在 JSFiddle 上 DIY

颜色混合

透明度不能解决所有问题,如果需要和另一种颜色混合(单纯与黑白混合可以考虑使用 HSL 模型),或者需要渐变,就只能使用一些「黑科技」了。

比如说,想把背景颜色设置为 50% 文字颜色 + 50% 主色:

... {
  --base-color: var(--text-color);
  --mix-color: rgba(
    var(--primary-color-r),
    var(--primary-color-g),
    var(--primary-color-b),
    .5
  );
  background-color: var(--base-color);
  background-image: linear-gradient(
    to bottom,
    var(--mix-color),
    var(--mix-color)
  );
}

已知问题

除了用起来不如 CSS 预处理器方便之外,Safari 在某些情况下无法工作。比如说

:root {
  --r: 255;
  --g: 0;
  --b: 0;
}

.foo {
  border: 10px solid
    rgba(var(--r), var(--g), var(--b), .5);
}

在 Safari 下边框会被渲染为 currentColor 而不是半透明的红色。

解决方法很简单,在内部多定义一个 CSS 变量即可。

在 JSFiddle 上 DIY(请对比在 Chrome 中和 Safari 中的表现)