使用 CSS 自定义属性构建一个色彩方案

发布于 大漠

最近在团队内搞Design Token 输出 CSS 或 JavaScript 变量的一些事情,希望使用 Design Tokens 来描述一个组件的可变参数,从而基于一个 UI 设计能动态输出多个 UI 效果。为此从颜色系统这一块着手,希望使用 Design Token 和 CSS 变量能构建一个具有可访问性的颜色系统。在这篇文章中想和大家一起聊聊这方面的话题,希望大家感兴趣。

目标

我们今天的目标是从一个基本的品牌颜色开始,使用 CSS 自定义属性calc() 来建立一个具有可访问的颜色系统,以制作一个适应用户偏好的 Web 页面或应用。在此基础上,再一起探讨Design Token如何描述一个色彩方案,并且怎么将Token转换成我们可用的CSS自定义属性!

先从品牌色开始吧!

品牌颜色

通常对于一个 Web 页面或应用而言,他的品牌颜色是已经确定的,并以会以Web开发者熟悉的颜色表达方式交付,比如十六进制或RGB或HSL。

拿其中一个品牌颜色为例,比如上图中的 #7f67be,其对应的是 hsl(257 40% 57%)。我们可以使用三个 CSS 自定义属性来描述 HSL 颜色的每个通道值,比如 hsl(257 40% 57%) 可以像下面这样来描述:

:root {
    --primary-hue: 257;
    --primary-saturation: 40%;
    --primary-lightness: 57%;
}

CSS 可以基于这些颜色属性进行计算,如 calc(var(--primary-lightness) - 20%) 可以将亮度值降低 20%,使品牌颜色变深或变浅。按这样的方式可以构建一个色彩方案的基础,因为 CSS 可以通过调整 HSL 的饱和度和亮度来使所有的颜色保持在同一个色系中。

场景中的颜色体系

在实际开发的过程中,可能会为不同的场景提供不同的颜色体系,如下图所示:

特别是因为 macOS系统提出DarkMode模式之后,这种类似换肤的模式越来越流行:

为此,我们可以为每种颜色的变体添加相应的标记来匹配相应的方案。比如:

  • 亮色模式下添加 -light后缀
  • 暗色模式下添加 -dark 后缀
  • 昏暗模式(介于亮色和暗色模式之间)下添加 -dim后缀

比如:

:root {
    --primary-hue: 257;
    --primary-saturation: 40%;
    --primary-lightness: 57%;
    --primary-light: hsl(var(--primary-hue) var(--primary-saturation) var(--primary-lightness));
    --primary-dark: hsl(var(--primary-hue)  calc(var(--primary-saturation) / 2)  calc(var(--primary-lightness) / 1.5));
    --primary-dim: hsl(var(--primary-hue)  calc(var(--primary-saturation) / 1.25)  calc(var(--primary-lightness) / 1.25));
}

接下来,我们就分别来看看亮色(-light)、暗色(-dark)和昏暗(-dim)下颜色体系的建立。

亮色系主题

先从品牌色 primary 开始(#D4CCFF),即使用hsl()颜色模式,包括--primary-hue(色相)、--primary-saturation(饱和度)和 --primary-lightness(亮度)来描述--primary-light颜色,在这个过程中不会涉及任何calc()的计算:

:root {
    --primary-hue: 250;
    --primary-saturation: 100%;
    --primary-lightness: 90%;
    --primary-light: hsl(
        var(--primary-hue) var(--primary-saturation) var(--primary-lightness)
    );
}

接下来,给配色方案中的文本元素添加颜色。在亮色主题中,文本颜色应该是暗色的,即调整颜色中的亮度(即 hsl中的L),建议低于50%。比如:

* {
    --primary-hue: 250;
    --primary-saturation: 100%;
    --primary-lightness: 90%;
    --primary-light: hsl(
        var(--primary-hue) var(--primary-saturation) var(--primary-lightness)
    );
    --on-primary-light: hsl(var(--primary-hue) var(--primary-saturation) 10%)
}

div {
    background-color: var(--primary-light);
    color: var(--on-primary-light);
}

有关于颜色对比度和Web可访问性更多的介绍,可以阅读《颜色对比度和Web可访问性》一文!

再来看表面颜色(Surface),就是文本所处的背景、边框和其他装饰性的表面,而且文本放在其中。在一个亮色的主题中,表面颜色是一个亮色,而文本的颜色则是深色。如果使用 hsl() 来创建亮色的话,只需要将亮度(L)调高,越接近100%,颜色越亮。同时还可以降低颜色的饱和度(S),让整个颜色看起来不会太大的色差:

:root {
    --primary-hue: 250;
    --primary-saturation: 100%;
    --primary-lightness: 90%;
    
    --surface1-light: hsl(var(--primary-hue) 40% 90%);
    --surface2-light: hsl(var(--primary-hue) 30% 99%);
    --surface3-light: hsl(var(--primary-hue) 30% 92%);
    --surface4-light: hsl(var(--primary-hue) 30% 85%);
}

这些颜色(surface*)是因为装饰性的颜色往往需要更多的变体,它们可以被用于一些控制的状态中,比如悬浮状态(hover)或焦点状态(focus)。将--surface2-light用于悬浮状态,--surface3-light用于焦点状态。

最后再来看一个色彩方案中的阴影部分。阴影主要作用就是能增加UI表现的层次性。色彩方案中的阴影颜色一般使用色调的自定义属性,略带饱和的色调,而且偏暗。就我们示例而言,就是建立一个非常暗的略带浅紫色的阴影。

:root {
    --primary-hue: 250;
    --primary-saturation: 100%;
    --primary-lightness: 90%;
    
    --surface-shadow-light: var(--primary-hue) 40% 10%;
    --shadow-strength-light: 0.02;
}

注意,示例中的阴影颜色--surface-shadow-light 并没有放到 hsl() 函数中。这是因为阴影颜色常会带有透明能道,比如示例中的--shadow-strength-light值。即:

div {
    box-shadow: 0 2.8px 2.2px
        hsl(
            var(--surface-shadow-light) / calc(var(--shadow-strength-light) + 0.03)
        ),
        0 6.7px 5.3px
        hsl(
            var(--surface-shadow-light) / calc(var(--shadow-strength-light) + 0.01)
        ),
        0 12.5px 10px
        hsl(
            var(--surface-shadow-light) / calc(var(--shadow-strength-light) + 0.02)
        ),
        0 22.3px 17.9px
        hsl(
            var(--surface-shadow-light) / calc(var(--shadow-strength-light) + 0.02)
        ),
        0 41.8px 33.4px
        hsl(
            var(--surface-shadow-light) / calc(var(--shadow-strength-light) + 0.03)
        ),
        0 100px 80px hsl(var(--surface-shadow-light) / var(--shadow-strength-light));
}

示例中使用了多阴影,如果我手撸不出一个效果较好的阴影,还可以借助相应的工具来完成,比如@Philipp Brumm的《smooth box shadow generator inspired by this article》和@JoshWComeau的《Shadow Palette Generator》:

把他们放到一起:

:root {
    --primary-hue: 250;
    --primary-saturation: 100%;
    --primary-lightness: 90%;
    --primary-light: hsl(
        var(--primary-hue) var(--primary-saturation) var(--primary-lightness)
    );
    --on-primary-light: hsl(var(--primary-hue) var(--primary-saturation) 10%);

    --surface1-light: hsl(var(--primary-hue) 40% 90%);
    --surface2-light: hsl(var(--primary-hue) 30% 99%);
    --surface3-light: hsl(var(--primary-hue) 30% 92%);
    --surface4-light: hsl(var(--primary-hue) 30% 85%);

    --surface-shadow-light: var(--primary-hue) 40% 10%;
    --shadow-strength-light: 0.02;
}

暗色系主题

除了亮色系之外,有的时候为了满足用户的偏好设置,在开发Web应用的时候还会提供品牌色的变体,比如暗色系。我们可以根据前面亮色系的方式来构建一个暗色系:

:root {
    --primary-hue: 256;
    --primary-saturation: 34%;
    --primary-lightness: 48%;
    --primary-dark: hsl(
        var(--primary-hue) var(--primary-saturation) var(--primary-lightness)
    );
    --on-primary-dark: hsl(var(--primary-hue) var(--primary-saturation) 90%);

    --surface1-dark: hsl(var(--primary-hue) 40% 50%);
    --surface2-dark: hsl(var(--primary-hue) 38% 46%);
    --surface3-dark: hsl(var(--primary-hue) 36% 41%);
    --surface4-dark: hsl(var(--primary-hue) 34% 30%);
    --surface-shadow-dark: var(--primary-hue) 40% 20%;
    --shadow-strength-dark: 0.02;
}

注意,现在我们使用-light-dark来做为色系的区分:

其他系色

如果还有其他场景采用另外一种色系或更多的场景,我们可能要维护的 CSS 自定义属性会更多,比如:

.dim {
    --primary-hue: 250;
    --primary-saturation: 100%;
    --primary-lightness: 90%;
    --primary-dim: hsl(
        var(--primary-hue) calc(var(--primary-saturation) / 1.5)
        calc(var(--primary-lightness) / 3.5)
    );
    --on-primary-dim: hsl(var(--primary-hue) 35% 90%);

    --surface1-dim: hsl(var(--primary-hue) 40% 20%);
    --surface2-dim: hsl(var(--primary-hue) 40% 25%);
    --surface3-dim: hsl(var(--primary-hue) 30% 30%);
    --surface4-dim: hsl(var(--primary-hue) 30% 35%);

    --surface-shadow-dim: var(--primary-hue) 40% 10%;
    --shadow-strength-dim: 0.25;
}

优化代码

对于开发者而言,我们在使用描述颜色的自定义属性时,并不希望添加额外的后缀(比如-light-dark-dim等)来区分。更多的时候期望在使用颜色时像下面这样:

.box {
    background-color: var(--primary);
    color: var(--on-primary);
    box-shadow: 0 0 2px 4px
    hsl(var(--surface-shadow) / var(--shadow-strength));
}

我们可以借助CSS自定义属性的作用域名来优化自定义属性的使用:

:root {
    --primary-hue: 250;
    --primary-saturation: 100%;
    --primary-lightness: 90%;
}

[color-scheme="light"] {
    --primary: hsl(
        var(--primary-hue) var(--primary-saturation) var(--primary-lightness)
    );
    --on-primary: hsl(var(--primary-hue) var(--primary-saturation) 10%);

    --surface1: hsl(var(--primary-hue) 40% 90%);
    --surface2: hsl(var(--primary-hue) 30% 99%);
    --surface3: hsl(var(--primary-hue) 30% 92%);
    --surface4: hsl(var(--primary-hue) 30% 85%);

    --surface-shadow: var(--primary-hue) 40% 10%;
    --shadow-strength: 0.25;
}

[color-scheme="dark"] {
    --primary: hsl(
        var(--primary-hue) calc(var(--primary-saturation) / 2.5) calc(var(--primary-lightness) / 2.5)
    );
    --on-primary: hsl(var(--primary-hue) var(--primary-saturation) 90%);

    --surface1: hsl(var(--primary-hue) 40% 50%);
    --surface2: hsl(var(--primary-hue) 38% 46%);
    --surface3: hsl(var(--primary-hue) 36% 41%);
    --surface4: hsl(var(--primary-hue) 34% 30%);
    --surface-shadow: var(--primary-hue) 40% 20%;
    --shadow-strength: 0.25;

} 

[color-scheme="dim"] {
    --primary: hsl(
        var(--primary-hue) calc(var(--primary-saturation) / 1.5)
        calc(var(--primary-lightness) / 3.5)
    );
    --on-primary: hsl(var(--primary-hue) 35% 90%);

    --surface1: hsl(var(--primary-hue) 40% 20%);
    --surface2: hsl(var(--primary-hue) 40% 25%);
    --surface3: hsl(var(--primary-hue) 30% 30%);
    --surface4: hsl(var(--primary-hue) 30% 35%);

    --surface-shadow: var(--primary-hue) 40% 10%;
    --shadow-strength: 0.25;
}

.box {
    background-color: var(--primary);
    color: var(--on-primary);
}

我们在容器上添加color-scheme来区分:

<html color-scheme="light">
    <body>
        <div class="box"></div>
    </body>
</html>

<html color-scheme="dark">
    <body>
        <div class="box"></div>
    </body>
</html>

<html color-scheme="dim">
    <body>
        <div class="box"></div>
    </body>
</html>

你将看到的效果如下:

除此之外,还可以借助 CSS 媒体查询新特性,比如 prefers-color-schemeprefers-contrast,可以让颜色色跟着用户偏好设置改变颜色:

/* Default: Light*/
:root {
    --primary-hue: 250;
    --primary-saturation: 100%;
    --primary-lightness: 90%;
}

html {
    --primary: hsl(
        var(--primary-hue) var(--primary-saturation) var(--primary-lightness)
    );
    --on-primary: hsl(var(--primary-hue) var(--primary-saturation) 10%);

    --surface1: hsl(var(--primary-hue) 40% 90%);
    --surface2: hsl(var(--primary-hue) 30% 99%);
    --surface3: hsl(var(--primary-hue) 30% 92%);
    --surface4: hsl(var(--primary-hue) 30% 85%);

    --surface-shadow: var(--primary-hue) 40% 10%;
    --shadow-strength: 0.25;
}

@media (prefers-color-scheme: dark) {
    html {
        --primary: hsl(
            var(--primary-hue) calc(var(--primary-saturation) / 2.5) calc(var(--primary-lightness) / 2.5)
        );
        --on-primary: hsl(var(--primary-hue) var(--primary-saturation) 90%);

        --surface1: hsl(var(--primary-hue) 40% 50%);
        --surface2: hsl(var(--primary-hue) 38% 46%);
        --surface3: hsl(var(--primary-hue) 36% 41%);
        --surface4: hsl(var(--primary-hue) 34% 30%);
        --surface-shadow: var(--primary-hue) 40% 20%;
        --shadow-strength: 0.25;
    }
}

@media (prefers-contrast: less) {
    html {
        --primary: hsl(
            var(--primary-hue) calc(var(--primary-saturation) / 1.5)
            calc(var(--primary-lightness) / 3.5)
        );
        --on-primary: hsl(var(--primary-hue) 35% 90%);

        --surface1: hsl(var(--primary-hue) 40% 20%);
        --surface2: hsl(var(--primary-hue) 40% 25%);
        --surface3: hsl(var(--primary-hue) 30% 30%);
        --surface4: hsl(var(--primary-hue) 30% 35%);

        --surface-shadow: var(--primary-hue) 40% 10%;
        --shadow-strength: 0.25;
    }
}

使用Token来描述颜色体系

在很多设计系统或一些UI框架中会有一些关于颜色的面板描述,比如这两年比较流行的 TailwindcssWindi CSS 都有这方面的定义和描述:

也有些团队或设计系统中会以自己的品牌色来定义一个颜色相关的面板:

对于这些,我们都可以使用 Design Token来定义。那么Design Token是什么?

Design Token 是什么?

不少同学可能会认为 Design Token 只是一个与设计有关的变量的命名。事实上,不同的领域对 Design Token 的描述是不同的。我们来看一些常见的定义,以了解这种混乱的情况。

W3C 是这样描述 Design Token的:

Design Token 是设计系统(Design System)中不可分割的部分,如颜色、间距、排版等。

Mozilla 是这样定义 Design Token 的:

Design Token 是视觉属性(Visual Property)的抽象,比如颜色、字体、宽度、动画等。这些原始值与语言无关,一旦经过转换或格式化,就可以在任何平台上使用。

Design Token 之父 @Salesforce 将其定义为

Design Token 是设计系统(Design System)的视觉设计原子(Visual Design Atoms)。具体来说,它们是存储视觉设计的命名实体,用来替代硬编码的值,以便为 UI 开发(UI Development)维护一个可扩展的、一致的视觉系统(Visual System)。

根据上面的描述,我们可以对 Design Token 有一个较为清晰的定义:

Design Token 是命名的实体,它存储了原始的、不可分割的设计值。它们是设计系统的核心部分。并以一种与技术无关的格式存储,它们可以在任何平台上进行转换使用,以取代硬编码的值。

也就是说,把 Design Token 只看作是变量,就好比说响应式设计是媒体查询。事实上,它是一个与技术无关的架构和过程,用于在多个平台和设备上的扩展设计。它更像是一种 方法论。把 Design Token 上升为一种方法论,它可以改变整个行业。

方法论是一种我们如何做某事的方法,而不仅仅是一种新的工具。

不过,我们在这里不会花时间和大家探讨 Design Token,但它的组织结构对于我们来说是非常实用的:

用代码来描述的话,可能会像下面这样:

:root {
    --purple-500: #5843f5;
    --color-action: #5843f5;
    --color-background-button: #5843f5;
}

我们也可以像下面这样来描述:

:root {
    --purple-500:  #5843f5;
    --color-action: var(--purple-500);
    --color-background-button: var(--color-action);
}

因此,我更喜欢按下面这样的方式来定义和划分 Design Token:

你可能已经感觉到了Design Token 的优势了。事实上,除此之外,我们可以使用 JSON 或 YAML 格式来定义 Design Token,甚至是来描述一个设计指南。

使用JSON来描述一个颜色面板

Design Token 一个最大优势是可以使用 JSON 或 YAML 来定义,比如说,我们要使用 JSON 来描述一个品牌色中的 primary,可以像下面这样来定义:

// ./token/color/colors.json
{
    "ref": {
        "color": {
            "primary": {
                "primary0": {
                    "value": "#000000",
                    "type": "color",
                    "description": null
                },
                "primary10": {
                    "value": "#21005D",
                    "type": "color",
                    "description": null
                },
                "primary20": {
                    "value": "#381E72",
                    "type": "color",
                    "description": null
                },
                "primary30": {
                    "value": "#4F378B",
                    "type": "color",
                    "description": null
                },
                "primary40": {
                    "value": "#6750A4",
                    "type": "color",
                    "description": null
                },
                "primary50": {
                    "value": "#7F67BE",
                    "type": "color",
                    "description": null
                },
                "primary60": {
                    "value": "#9A82DB",
                    "type": "color",
                    "description": null
                },
                "primary70": {
                    "value": "#B69DF8",
                    "type": "color",
                    "description": null
                },
                "primary80": {
                    "value": "#D0BCFF",
                    "type": "color",
                    "description": null
                },
                "primary90": {
                    "value": "#EADDFF",
                    "type": "color",
                    "description": null
                },
                "primary95": {
                    "value": "#F6EDFF",
                    "type": "color",
                    "description": null
                },
                "primary99": {
                    "value": "#FFFBFE",
                    "type": "color",
                    "description": null
                },
                "primary100": {
                    "value": "#FFFFFF",
                    "type": "color",
                    "description": null
                }
            }
        }
    }
}

使用相关的编译工具(比如 Style Dictionary) 可以编译出相应平台需要的代码,比如 CSS 自定义属性:

:root {
    --ref-color-primary-primary0: #000000;
    --ref-color-primary-primary10: #21005D;
    --ref-color-primary-primary20: #381E72;
    --ref-color-primary-primary30: #4F378B;
    --ref-color-primary-primary40: #6750A4;
    --ref-color-primary-primary50: #7F67BE;
    --ref-color-primary-primary60: #9A82DB;
    --ref-color-primary-primary70: #B69DF8;
    --ref-color-primary-primary80: #D0BCFF;
    --ref-color-primary-primary90: #EADDFF;
    --ref-color-primary-primary95: #F6EDFF;
    --ref-color-primary-primary99: #FFFBFE;
    --ref-color-primary-primary100: #FFFFFF;
}

我们还可以添加第二层:

{
    "ref": {
        "color": {
            "primary": {
                "primary0": {
                    "value": "#000000",
                    "type": "color",
                    "description": null
                },
                "primary10": {
                    "value": "#21005D",
                    "type": "color",
                    "description": null
                },
                "primary20": {
                    "value": "#381E72",
                    "type": "color",
                    "description": null
                },
                "primary30": {
                    "value": "#4F378B",
                    "type": "color",
                    "description": null
                },
                "primary40": {
                    "value": "#6750A4",
                    "type": "color",
                    "description": null
                },
                "primary50": {
                    "value": "#7F67BE",
                    "type": "color",
                    "description": null
                },
                "primary60": {
                    "value": "#9A82DB",
                    "type": "color",
                    "description": null
                },
                "primary70": {
                    "value": "#B69DF8",
                    "type": "color",
                    "description": null
                },
                "primary80": {
                    "value": "#D0BCFF",
                    "type": "color",
                    "description": null
                },
                "primary90": {
                    "value": "#EADDFF",
                    "type": "color",
                    "description": null
                },
                "primary95": {
                    "value": "#F6EDFF",
                    "type": "color",
                    "description": null
                },
                "primary99": {
                    "value": "#FFFBFE",
                    "type": "color",
                    "description": null
                },
                "primary100": {
                    "value": "#FFFFFF",
                    "type": "color",
                    "description": null
                }
            }
        }
    },
    "sys": { 
        "color": {
            "light": {
                "primary": {
                    "value": "{ref.color.primary.primary40.value}",
                    "type": "color",
                    "description": "primary40: #6750A4"
                },
                "on-primary": {
                    "value": "{ref.color.primary.primary100.value}",
                    "type": "color",
                    "description": "primary100: #FFFFFF"
                }
            },
            "dark": {
                "primary": {
                    "value": "{ref.color.primary.primary80.value}",
                    "type": "color",
                    "description": "primary80: #D0BCFF"
                },
                "on-primary": {
                    "value": "{ref.color.primary.primary20.value}",
                    "type": "color",
                    "description": "primary20: #381E72"
                }
            }
        }
    }
}

编译出来的 CSS 自定义属性:

:root {
    --ref-color-primary-primary0: #000000;
    --ref-color-primary-primary10: #21005D;
    --ref-color-primary-primary20: #381E72;
    --ref-color-primary-primary30: #4F378B;
    --ref-color-primary-primary40: #6750A4;
    --ref-color-primary-primary50: #7F67BE;
    --ref-color-primary-primary60: #9A82DB;
    --ref-color-primary-primary70: #B69DF8;
    --ref-color-primary-primary80: #D0BCFF;
    --ref-color-primary-primary90: #EADDFF;
    --ref-color-primary-primary95: #F6EDFF;
    --ref-color-primary-primary99: #FFFBFE;
    --ref-color-primary-primary100: #FFFFFF;
    --sys-color-light-primary: #6750A4;
    --sys-color-light-on-primary: #FFFFFF;
    --sys-color-dark-primary: #D0BCFF;
    --sys-color-dark-on-primary: #381E72;
}

甚至调整相关的编译脚本,你可以编译出像下面这样的CSS自定义属性:

:root {
    --sys-color-light-primary: #6750A4;
    --sys-color-light-on-primary: #FFFFFF;
}

[color-scheme="light"] {
    --sys-color-light-primary: #6750A4;
    --sys-color-light-on-primary: #FFFFFF;
}

@media (prefers-color-scheme: dark) {
    :root {
        --sys-color-dark-primary: #D0BCFF;
        --sys-color-dark-on-primary: #381E72; 
    }
}

[color-scheme="dark"] {
    --sys-color-dark-primary: #D0BCFF;
    --sys-color-dark-on-primary: #381E72; 
}

或者编译出:

:root {
    --primary: #6750A4;
    --on-primary: #FFFFFF;
}

[color-scheme="light"] {
    --primary: #6750A4;
    --on-primary: #FFFFFF;
}

@media (prefers-color-scheme: dark) {
    :root {
        --primary: #D0BCFF;
        --primary: #381E72; 
    }
}

[color-scheme="dark"] {
    --primary: #D0BCFF;
    --primary: #381E72; 
}

按照上面的方式,可以很轻易的使用 Design Token 来描述不同场景下组件的颜色体系,比如像下面这个按钮的效果:

{
    "button": {
        "base": {
            "default": {
                "background-color": {
                    "value": "rgb(125 196 232)"
                },
                "text-color": {
                    "value": "rgb(26 26 26)"
                },
                "border-color": {
                    "value": "rgb(103 187 228)"
                }
            },
            "hovered": {
                "background-color": {
                    "value": "rgb(110 201 247)"
                },
                "text-color": {
                    "value": "rgb(26 26 26)"
                },
                "border-color": {
                    "value": "rgb(13 166 242)"
                }
            },
            "focused": {
                "background-color": {
                    "value": "rgb(168 216 240)"
                },
                "text-color": {
                    "value": "rgb(26 26 26)"
                },
                "border-color": {
                    "value": "rgb(82 177 224)"
                }
            },
            "disabled": {
                "background-color": {
                    "value": "rgb(184 211 224)"
                },
                "text-color": {
                    "value": "rgb(26 26 26 / 50%)"
                },
                "border-color": {
                    "value": "rgb(148 189 209)"
                }
            }
        },
        "primary":  {
            "default": {
                "background-color": {
                    "value": "rgb(204 168 240)"
                },
                "text-color": {
                    "value": "rgb(26 26 26)"
                },
                "border-color": {
                    "value": "rgb(191 147 236)"
                }
            },
            "hovered": {
                "background-color": {
                    "value": "rgb(204 158 250)"
                },
                "text-color": {
                    "value": "rgb(26 26 26)"
                },
                "border-color": {
                    "value": "rgb(153 61 245)"
                }
            },
            "focused": {
                "background-color": {
                    "value": "rgb(184 224 204)"
                },
                "text-color": {
                    "value": "rgb(26 26 26)"
                },
                "border-color": {
                    "value": "rgb(112 194 153)"
                }
            },
            "disabled": {
                "background-color": {
                    "value": "rgb(230 219 240)"
                },
                "text-color": {
                    "value": "rgb(26 26 26 / 50%)"
                },
                "border-color": {
                    "value": "rgb(204 184 224)"
                }
            }
        },
        "secondary": {
            "default": {
                "background-color": {
                    "value": "rgb(148 209 179)"
                },
                "text-color": {
                    "value": "rgb(26 26 26)"
                },
                "border-color": {
                    "value": "rgb(130 201 166)"
                }
            },
            "hovered": {
                "background-color": {
                    "value": "rgb(133 224 179)"
                },
                "text-color": {
                    "value": "rgb(26 26 26)"
                },
                "border-color": {
                    "value": "rgb(51 204 128)"
                }
            },
            "focused": {
                "background-color": {
                    "value": "rgb(184 224 204)"
                },
                "text-color": {
                    "value": "rgb(26 26 26)"
                },
                "border-color": {
                    "value": "rgb(112 194 153)"
                }
            },
            "disabled": {
                "background-color": {
                    "value": "rgb(199 209 204)"
                },
                "text-color": {
                    "value": "rgb(26 26 26 / 50%)"
                },
                "border-color": {
                    "value": "rgb(171 186 179)"
                }
            }
        },
        "complementary": {
            "default": {
                "background-color": {
                    "value": "rgb(228 145 103)"
                },
                "text-color": {
                    "value": "rgb(26 26 26)"
                },
                "border-color": {
                    "value": "rgb(224 129 82)"
                }
            },
            "hovered": {
                "background-color": {
                    "value": "rgb(246 139 85)"
                },
                "text-color": {
                    "value": "rgb(26 26 26)"
                },
                "border-color": {
                    "value": "rgb(218 80 11)"
                }
            },
            "focused": {
                "background-color": {
                    "value": "rgb(236 176 147)"
                },
                "text-color": {
                    "value": "rgb(26 26 26)"
                },
                "border-color": {
                    "value": "rgb(221 113 60)"
                }
            },
            "disabled": {
                "background-color": {
                    "value": "rgb(217 183 166)"
                },
                "text-color": {
                    "value": "rgb(26 26 26 / 50%)"
                },
                "border-color": {
                    "value": "rgb(201 154 130)"
                }
            }
        }  
    }
}

我们可以得到按钮组件所需的CSS自定义属性:

:root {
    --button-base-default-background-color: rgb(125 196 232);
    --button-base-default-text-color: rgb(26 26 26);
    --button-base-default-border-color: rgb(103 187 228);
    --button-base-hovered-background-color: rgb(110 201 247);
    --button-base-hovered-text-color: rgb(26 26 26);
    --button-base-hovered-border-color: rgb(13 166 242);
    --button-base-focused-background-color: rgb(168 216 240);
    --button-base-focused-text-color: rgb(26 26 26);
    --button-base-focused-border-color: rgb(82 177 224);
    --button-base-disabled-background-color: rgb(184 211 224);
    --button-base-disabled-text-color: rgb(26 26 26 / 50%);
    --button-base-disabled-border-color: rgb(148 189 209);
    --button-primary-default-background-color: rgb(204 168 240);
    --button-primary-default-text-color: rgb(26 26 26);
    --button-primary-default-border-color: rgb(191 147 236);
    --button-primary-hovered-background-color: rgb(204 158 250);
    --button-primary-hovered-text-color: rgb(26 26 26);
    --button-primary-hovered-border-color: rgb(153 61 245);
    --button-primary-focused-background-color: rgb(184 224 204);
    --button-primary-focused-text-color: rgb(26 26 26);
    --button-primary-focused-border-color: rgb(112 194 153);
    --button-primary-disabled-background-color: rgb(230 219 240);
    --button-primary-disabled-text-color: rgb(26 26 26 / 50%);
    --button-primary-disabled-border-color: rgb(204 184 224);
    --button-secondary-default-background-color: rgb(148 209 179);
    --button-secondary-default-text-color: rgb(26 26 26);
    --button-secondary-default-border-color: rgb(130 201 166);
    --button-secondary-hovered-background-color: rgb(133 224 179);
    --button-secondary-hovered-text-color: rgb(26 26 26);
    --button-secondary-hovered-border-color: rgb(51 204 128);
    --button-secondary-focused-background-color: rgb(184 224 204);
    --button-secondary-focused-text-color: rgb(26 26 26);
    --button-secondary-focused-border-color: rgb(112 194 153);
    --button-secondary-disabled-background-color: rgb(199 209 204);
    --button-secondary-disabled-text-color: rgb(26 26 26 / 50%);
    --button-secondary-disabled-border-color: rgb(171 186 179);
    --button-complementary-default-background-color: rgb(228 145 103);
    --button-complementary-default-text-color: rgb(26 26 26);
    --button-complementary-default-border-color: rgb(224 129 82);
    --button-complementary-hovered-background-color: rgb(246 139 85);
    --button-complementary-hovered-text-color: rgb(26 26 26);
    --button-complementary-hovered-border-color: rgb(218 80 11);
    --button-complementary-focused-background-color: rgb(236 176 147);
    --button-complementary-focused-text-color: rgb(26 26 26);
    --button-complementary-focused-border-color: rgb(221 113 60);
    --button-complementary-disabled-background-color: rgb(217 183 166);
    --button-complementary-disabled-text-color: rgb(26 26 26 / 50%);
    --button-complementary-disabled-border-color: rgb(201 154 130);
}

可以设置按钮组件边框颜色、背景色和文本色:

.button {
    --background-color: var(--button-base-default-background-color);
    --text-color: var(--button-base-default-text-color);
    --border-color: var(--button-base-default-border-color);
    background-color: var(--background-color);
    color: var(--text-color);
    border: 2px solid var(--border-color);
}

.button:hover {
    --background-color: var(--button-base-hovered-background-color);
    --text-color: var(--button-base-hovered-text-color)
    --border-color: var(--button-base-hovered-border-color)
}

.button:focus {
    --background-color: var(--button-base-focused-background-color);
    --text-color: var(--button-base-focused-text-color);
    --border-color: var(--button-base-focused-border-color);
    border: 2px solid var(--border-color);
}

.button.disabled {
    --background-color: var(--button-base-disabled-background-color);
    --text-color: var(--button-base-disabled-text-color);
    --border-color: var(--button-base-disabled-border-color);
}

在其他变体下,只需要改变 --background-color--text-color--border-color即:

.primary {
    --background-color: var(--button-primary-default-background-color);
    --text-color: var(--button-primary-default-text-color);
    --border-color: var(--button-primary-default-border-color);
}

.primary:hover {
    --background-color: var(--button-primary-hovered-background-color);
    --text-color: var(--button-primary-hovered-text-color)
    --border-color: var(--button-primary-hovered-border-color)
}

.primary:focus {
    --background-color: var(--button-primary-focused-background-color);
    --text-color: var(--button-primary-focused-text-color);
    --border-color: var(--button-primary-focused-border-color);
}

.primary.disabled {
    --background-color: var(--button-primary-disabled-background-color);
    --text-color: var(--button-primary-disabled-text-color);
    --border-color: var(--button-primary-disabled-border-color);
}


.secondary {
    --background-color: var(--button-secondary-default-background-color);
    --text-color: var(--button-secondary-default-text-color);
    --border-color: var(--button-secondary-default-border-color);
}

.secondary:hover {
    --background-color: var(--button-secondary-hovered-background-color);
    --text-color: var(--button-secondary-hovered-text-color)
    --border-color: var(--button-secondary-hovered-border-color)
}

.secondary:focus {
    --background-color: var(--button-secondary-focused-background-color);
    --text-color: var(--button-secondary-focused-text-color);
    --border-color: var(--button-secondary-focused-border-color);
}

.secondary.disabled {
    --background-color: var(--button-secondary-disabled-background-color);
    --text-color: var(--button-secondary-disabled-text-color);
    --border-color: var(--button-secondary-disabled-border-color);
}

.complementary {
    --background-color: var(--button-complementary-default-background-color);
    --text-color: var(--button-complementary-default-text-color);
    --border-color: var(--button-complementary-default-border-color);
}

.complementary:hover {
    --background-color: var(--button-complementary-hovered-background-color);
    --text-color: var(--button-complementary-hovered-text-color)
    --border-color: var(--button-complementary-hovered-border-color)
}

.complementary:focus {
    --background-color: var(--button-complementary-focused-background-color);
    --text-color: var(--button-complementary-focused-text-color);
    --border-color: var(--button-complementary-focused-border-color);
}

.complementary.disabled {
    --background-color: var(--button-complementary-disabled-background-color);
    --text-color: var(--button-complementary-disabled-text-color);
    --border-color: var(--button-complementary-disabled-border-color);
}

效果如下:

除了使用 Style Dictionary 把描述 Design Token 的 JSON 编译出 CSS 自定义属性之外,还可以 Open Props 实现同样的效果:

小结

可以说,我们已完成了今天所设定的目标。使用 CSS 自定义属生、calc()函数结合hsl()创建一个具有可访问性的颜色方案,并且还能适用于用户偏好设置。而且我们还引入了 Design Token,使用类似 Style Dictionary 和 Open Props 工具(库)将定义颜色方案的 JSON转换出适用于 Web 的CSS自定义属性,运用于生产中。事实上,Design Token 除了可以用来描述颜色之外,还可以是其他的设计样式。

Design Token 是非常有意思的,在构建设计系统中非常的常见。但我们今天的主题并不是 Design Token,所以并未对其进行展开的讨论。不过,不用担心,如果你对这方面的话题感兴趣的话,请持续关注后续相关的更新。我想不会令你感到失望的!