自定义主题色实现之 CSS Variable

背景

  • 需求背景:动态配置页面主题相关颜色,而不是定制主题模式(类似黑夜模式,白天模式)。
  • 兼容性问题:在 IE 中 var()报错显示异常成透明色,(css-vars-ponyfill 插件 ,使页面展示一个默认色)
  • 第三方 UI 库支持 CSS Variable,(antd@4.17.0-alpha.0 以上,mkui-fd@4.1.8-beta 以上 )

1. 实现原理

1.1. 自定义属性 (--*): CSS 变量

带有前缀 -- 的属性名,比如--example--name,表示的是带有值的自定义属性,其可以通过var 函数在全文档范围内复用的。

CSS 自定义属性是可以级联的:每一个自定义属性可以多次出现,并且变量的值将会借助级联算法和自定义属性值运算出来。

注意,规则集所指定的选择器定义了自定义属性的可见作用域。通常的最佳实践是定义在根伪类:root 下,这样就可以在 HTML 文档的任何地方访问到它。

:root 这个 CSS 伪类匹配文档树的根元素。对于 HTML 来说,:root 表示 <html> 元素,除了优先级更高之外,与 html 选择器相同。

1.2. ConfigProvider.config() (config-provider/index.js)
  ConfigProvider.config = setGlobalConfig;
  var setGlobalConfig = function setGlobalConfig(_ref) {
  var prefixCls = _ref.prefixCls,
        iconPrefixCls = _ref.iconPrefixCls,
        theme = _ref.theme;

  if (prefixCls !== undefined) {
     globalPrefixCls = prefixCls;
  }

  if (iconPrefixCls !== undefined) {
     globalIconPrefixCls = iconPrefixCls;
  }

  if (theme) {
     (0, _cssVariables.registerTheme)(getGlobalPrefixCls(), theme); // 更改主题色
  }
1.3. registerTheme() (node_modules/mkui-fd/lib/config-provider/cssVariables.js)

创建所有 css 变量,可参考简易版 customCssVariable.js

import { updateCSS } from 'rc-util/lib/Dom/dynamicCSS';
import { TinyColor } from '@ctrl/tinycolor';
import { generate } from '@ant-design/colors';

function setThemeColor({ themeColor, varName }) {
  const variables = {};

  // ================ Primary Color ================
  if (themeColor) {
    // 转成 TinyColor 色值格式
    const primaryColor = new TinyColor(themeColor);
    // 10个不同阶梯色值
    const colorPalettes = generate(primaryColor.toRgbString());
    // Legacy - We should use semantic naming standard
    variables[`${varName}`] = themeColor;
    colorPalettes.forEach((color, index) => {
      variables[`${varName}-${index + 1}`] = color;
    });
  }

  // Convert to css variables
  // cssList ['--theme-color: xxx','--theme-color-1: xxx',...,'--theme-color-10: xxx' ]
  const cssList = Object.keys(variables).map(
    key => `--${key}: ${variables[key]};`,
  );

  // updateCSS 更新业务代码中的主题色: 创建 style 标签,值为cssList,并作为最后一个子元素插在head标签下
  updateCSS(
    '\n  :root {\n    '.concat(cssList.join('\n'), '\n  }\n  '),
    '-mkui-fd-'.concat(Date.now(), '-').concat('-dynamic-theme'),
  );
}
1.4. updateCSS() (node_modules/rc-util/lib/Dom/dynamicCSS.js)

创建 style 标签,值为 cssList,作为最后一个子元素插在 head 标签下,详见 node_modules/rc-util/lib/Dom/dynamicCSS.js

1.5. 动态改变颜色另外一种写法(我们项目中不推荐)

在 body 标签中增加 style 属性,改变 body 中所有 --mkui-primary-color 使用值, 优先级高于 html

document.body.style.setProperty('--mkui-primary-color', '#ffff00');

        ↓ ↓ ↓ ↓ ↓ ↓

<body style="--mkui-primary-color:#ffff00;">

2. 如何使用

2.1 替换当前项目引入样式文件为 CSS Variable 版本,并在 .babel.config 中去除 babel-plugin-import 配置。

{ "libraryName": "antd" }

     import { Button } from 'antd';

     ReactDOM.render(<Button>xxxx</Button>);

           ↓ ↓ ↓ ↓ ↓ ↓

     var button = require('antd/lib/button');
     ReactDOM.render(<button>xxxx</button>);

{ "libraryName": "antd", style: "css" }

     import { Button } from 'antd';

     ReactDOM.render(<Button>xxxx</Button>);

           ↓ ↓ ↓ ↓ ↓ ↓

     var button = require('antd/lib/button');
     require('antd/lib/button/style/css');
     ReactDOM.render(<button>xxxx</button>);

{ "libraryName": "antd", style: true }

     import { Button } from 'antd';

     ReactDOM.render(<Button>xxxx</Button>);

           ↓ ↓ ↓ ↓ ↓ ↓

     var button = require('antd/lib/button');
     require('antd/lib/button/style');
     ReactDOM.render(<button>xxxx</button>);

注意:

Antd 默认支持基于 ES modules 的 tree shaking,对于 js 部分,直接引入 import { Button } from 'antd' 就会有按需加载的效果。

如今webpack,rollup等构建工具都具备了摇树功能(Tree Shaking), 其原理是利用ESM模块的import语法的特性,通过AST语法树进行分析然后去除未使用到的代码。但tree shaking方式也只是处理了js部分,对于组件的css加载还需要手动引入.

2.2 引入包含css 变量的组件库 css 文件(app.js)
import 'mkui-fd/dist/mkui-fd.variable.min.css';
import 'mkui-ext/dist/mkui-ext.variable.min.css';
2.3.定义颜色变量(variables.less)
  1. 使用 mkui-fd 组件中已有变量

:root 下已经默认生成了部分可全局使用的变量:--mkui-primary-1, ... --mkui-primary-10

使用 var() 插入 CSS 变量的值

     @theme-color: var(--mkui-primary-color, #1890ff); 

     @theme-color-selected-bg: var(--mkui-primary-1, #f0f8ff);
  1. 自定义创建可全局使用的 CSS 变量

    // color.less

    为了兼容 IE 使用 css-vars-ponyfill, 必须使用:root 而不是 html

    :root {
    --theme-color: #1890ff;
    --theme-color-selected-bg: #f0f8ff;
    }

  // variables.less

  @theme-color: var(--theme-color);

  @theme-color-selected-bg: var(--theme-color-selected-bg, #f0f8ff);
2.4. 调用配置方法

1) 调用 ConfigProvider 配置方法设置主题色(cssVariable.js):

   // 修改 mkui-fd 组件颜色
   ConfigProvider.config({
     theme: { primaryColor: themeColor },
  });
  
  // 修改 mkui-ext 组件颜色
   ExtConfigProvider.config({
    theme: { primaryColor: themeColor },
  });

Note: primaryColor: 组件基本色,successColorwarningColorerrorColorinfoColor特定情况下的颜色。

2) 对于自定义颜色变量,动态更改自定义变量颜色(customCssVariable.js)

setThemeColor({ 
    themeColor, 
    varName: 'theme-color' 
})
2.5. 色值相关
  • 与主题色相关的颜色,设计图中会给一个主色(C6),及对应的10个色阶阶梯的哪一阶

3. 注意点:

  1. 为什么是:root 而不是 html

    • CSS 不仅用于样式化 HTML 文档, 它也用于 XML 和 SVG 文件。对于 XML 和 SVG 文件,:root 不是选择 html元素,而是选择它们的根(例如 SVG 文件中的 svg 标签)。

    • :root 选择器优先级更高

    • 有助于将使用的 CSS 变量与项目中使用样式的选择器分开

    • css-vars-ponyfill 中要求:自定义属性声明支持仅限于:root:host 规则集

  2. fade(),dark()等函数不支持变量参数

      fade('1199ff', 90%);
      fade(var(--theme-color), 90%); // 错误使用
    
  3. mkui-fd, mkui-ext以及项目中颜色相关样式,不要使用内联和 !important

  1. IE 11下取色器组件 react-color@2.19.3报错,降低react-color@2.18.1组件版本
    SCRIPT438: Object doesn't support property or method 'contains'

4. 其他扩展点

4.1 polyfill 和 ponyfill 的区别

polyfill 在原有的墙壁上打补丁,ponyfill 的策略则是另起炉灶,不会在原有的墙壁上修补,而是重新建一面墙,保证原来的墙壁还是原始纯净无污染。

例如:Array.isArray(), 此方法 IE8 浏览器并不支持

polyfill 策略

  if (!Array.isArray) {
     Array.isArray = function(arg) {
        return Object.prototype.toString.call(arg) === '[object Array]';
     };
  }

ponyfill 策略

  // 避免使用原生 API
  // 基本上,为了避免全局命名的污染,Ponyfill都是建议采用独立的模块化的方式开发与调用的
  function isArray (arg) {
     return Object.prototype.toString.call(arg) === '[object Array]';
  }

什么时候使用 Ponyfill?

  1. 有些原生 API 完全没法模拟,此时只能使用 Ponyfill 策略,例如 IE9 浏览器无法模拟 indexDB 能力,history.pushState()方法
  2. 有些原生 API 规范还没稳定,或者处于快速迭代中,或者是浏览器部分支持
4.2 css-vars-ponyfill 的使用

作用:

  • CSS 自定义属性到静态值的转换
  • 现代和旧版浏览器中运行时值的实时更新
  • 转换 <link><style>@import CSS
  • 将相对 url()路径转换为绝对 URL
  • 支持链式和嵌套的 var()函数
    ...

限制:

  • 自定义属性声明支持仅限于 :root:host 规则集
    • var() 的使用仅限于属性值(根据 W3C 规范)

更新值:

  • 在旧版浏览器中,ponyfill 将确定哪些 <link><style>中包含 css 变量,然后转换成与旧版兼容的 CSS,并附加到每个元素的 DOM。
  • 在支持 CSS 变量的现代浏览器中,ponyfill 将使用style.setProperty() 接口更新值。
    请注意,当 options.onlyLegacyfalse 时,支持 CSS 变量的现代浏览器将被视为旧版浏览器。

Note:
{ onlyLegacy: true }将此值设置为 false可以在现代浏览器中模仿旧版浏览器,进行测试和调试。

4.3 HSV


基于HSB模型构建色彩体系

1.色相

表示色彩的相貌,也就是我们常说的红、橙、黄、绿等颜色名称。色相值按位置度量,取值为0°~360°,在HSB色彩模型中红色为0°,黄色为60°,绿色为120°,青色为180°,蓝色为240°,品红色为300°。十二色相环每一色相间距30°,二十四色相环每一色相间距15°。

2.饱和度

表示色彩的纯度,取值范围0~100%,从色环中心向外递增。当饱和度为0时点在中心,则显示为灰、白、黑无彩色。当饱和度达到100%时,点则移动到色环边缘,会显示每个色相最纯的色光。如下图所示,在色相(H)、亮度(B)不变的情况下减少饱和度(S)颜色逐渐变淡最后变成白色。S控制混入白色的量。

3.亮度

指色彩的明亮度,取值范围0~100%,沿着圆柱体底部向上递增。亮度为0时即黑色,点处于最底部。当达到100%时点上升到顶端,会显示色相最鲜明的状态。如下图所示,在色相(H)、饱和度(S)不变的情况下减少亮度(B)颜色逐渐变暗最后变成黑色。B控制混入黑色的量。

4.结论

1. 选取一个颜色作为主色(6 号色);
2. 判断减淡或加深,进行颜色混合
    - 若减淡,则主色与纯白色(#fff)混合,根据色号,获取贝塞尔曲线上的对应值。
    - 若加深,则主色与它对应的深色混合,根据色号,获取贝塞尔曲线上的对应比例值。加深时主色对应的深色进行了明度与色相的调整,其中对色相的调整也就是上述引用中说的“针对冷暖色的旋转”;
3. 分别取1~9色号的色值,得到一条完整渐变色板。

参考文章

AntD 动态主题

基于 HSB 模型构建色彩体系

Ant Design 色板生成算法演进之路

polyfill 和 ponyfill

css-vars-ponyfill 文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容