你好,我是杨文坚。
在上一节课的Vue.js 3.x自研组件库的开发入门中我提到,组件库有一个重要的作用,就是“可定制化主题”。那么,什么是“可定制化主题”呢?
如果你在电商企业中进行业务功能的前端页面开发,原有使用的组件库是蓝色风格的样式,但是想在节假日里快速转变成红色风格的组件样式,再比如,如果你开发的页面是亮色系的效果,哪天产品经理需要前端快速实现暗色系的黑夜效果,提升用户夜间的使用体验,那么,你会怎么做前端页面的改造呢?
这些场景,都要处理前端页面整体颜色以及视觉风格的变化,这类“变化”在前端开发中一般定义为“主题”的控制,也就是“可定制化主题”。
作为负责业务需求的前端开发者,一般都尽量专注业务功能点的开发,页面的主题风格定制能力通常是在组件库中管理。那么,组件库的前端开发者,就需要提供一套能控制组件的主题风格的组件库,提供给业务前端开发者直接使用。这样,业务前端开发者不需要关心组件库的主题方案如何实现,只需要根据组件库的使用规范“开箱即用”就好。
那么,如何设计组件库的主题实现呢?我们先来看看主题方案设计需要做什么准备。
组件库的主题方案设计需要做什么准备?
既然是方案设计,首先要做的是方案的规范准备,这里主题的方案设计需要准备以下两种规范:
前面我们提到,页面主题的变化主要是整体颜色视觉风格的变化。而且,使用组件库开发,业务功能页面控制主题是通过组件库的内置主题系统来控制的。所以,我们主题方案设计的第一步就是需要设计好组件库的颜色规范。
这里要明确一点,颜色规范设计通常不是前端开发者的职责工作,而是设计师的工作。但是前端作为设计稿和实现代码之间的“桥梁”,需要做好设计稿的沟通和讨论。不过,设计师一般对颜色规范设计的流程比较严格,也有很多讲究。所以,作为前端开发者,我们有必要简单了解这些颜色规范设计的过程。
第一步是颜色种类的选择。一般设计师会选择几种大类型的颜色,例如红色、蓝色和绿色等。然后根据业务需要挑选这几类型中的一个基准色号。
第二步,基于上一步选择好的基准色号,进行颜色梯度的处理,例如颜色亮度和饱和度从浅到深的梯度处理。举个例子,灰色的颜色梯度处理如下图片所示:

最后一步,也就是第三步,根据不同颜色的颜色梯度,进行语义化使用处理。我们拿上述灰色每个梯度的颜色来“语义化处理”,将“灰色1号”作为页面背景颜色,“灰色10号”作为页面的字体颜色。效果如下述所示:

到这里,你可能会有疑惑,如果只是前端程序员自发建设组件库,没有设计师参与设计,要如何做颜色的设计规范呢?
这类无设计师的情况你也不用担心,有很多开源组件库都提供了现成的颜色设计规范。例如Ant Design官方团队的颜色规范、Element Plus官方团队的颜色规范都可以直接参考。
当然,这些颜色的设计规范都是面向设计师的,前端开发者在其中只是参与讨论,最终还是需要设计师来拍板敲定颜色的设计规范。
但接下来的将颜色规范的设计内容转变成代码的部分,就是前端开发者能拍板的领域了。
前端开发中对组件库的主题开发和控制主要基于CSS来处理的,所以在开发之前,我们要制定CSS的开发规范。
首先,我们要选择CSS的预处理器语言来开发,主要利用CSS预处理器语言的“可编程的逻辑语法”来编写CSS。目前主流的CSS的预处理器语言有Less和Sass,两者语法比较类似。
- Less,对 CSS原始语法增加了少许方便的扩展,例如函数、运算等语法,学习更容易
- 预处理器Sass,语法更加丰富和全面。
两种预处理器语言都有很多开源组件库都在使用,例如Ant Design使用了Less,Element Plus选择了Sass。这里我们主要选择Less来开发组件库的CSS代码,主要是考虑到Less简单易用,能满足绝大部分的开发需要。
我们可以用Less里的变量语法来管理所有的颜色梯度,如下述代码所示:
1 2 3 4 5 6 7 8 9 10
| @gray-1: #f5f5f5; @gray-2: #f0f0f0; @gray-3: #d9d9d9; @gray-4: #bfbfbf; @gray-5: #8c8c8c; @gray-6: #595959; @gray-7: #434343; @gray-8: #262626; @gray-9: #1f1f1f; @gray-10: #141414;
|
不过,虽然我们已经有了Less作为预处理器语法来管理CSS代码,例如主题颜色梯度都用了Less变量语法来管理,但接下来我们还要管理具体组件的主题样式上面,而且还要语义化控制到具体某个组件的某个维度的颜色。
什么是语义化颜色呢?一个按钮的背景颜色是“蓝色1号”,语义化颜色就是将“蓝色1号”语义化给了按钮背景颜色。但是实际中,组件语义化的内容维度是有很多层次的。
一个按钮的颜色,有背景颜色、字体颜色和边框颜色这一整套颜色体系,在按钮默认状态、点击状态和禁用状态时,背景、字体和边框颜色又需要独立的一套新的颜色体系。这个时候,就有三套颜色体系。如果再叠加一个按钮类型,例如是“实心颜色”的按钮和“空心颜色”的按钮,就演变成3x2的6种颜色体系。
你想,就这一个按钮组件,都有6种颜色体系,那如何做好CSS的语义化代码管理和维护呢?如果再来一个“白天”和“黑夜”模式的主题切换颜色功能,那要如何管理呢?
这个时候就需要用到CSS Variable 来管理语义化的组件颜色了。
什么是CSS Variable呢?就是 CSS 自定义属性,也可以称作CSS 变量或者级联变量,主要是CSS代码在浏览器中,定义一个CSS属性(或者称为“变量”)后,这个CSS属性就可以在页面中全局其它CSS代码中使用,甚至是覆盖重写。例如下面代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @prefix-name: my-vue;
@white: #ffffff; @black: #222222;
@gray-4: #bfbfbf;
:root { --@{prefix-name}-page-bg-color: @white; --@{prefix-name}-page-text-color: @black; --@{prefix-name}-page-border-color: @gray-4; }
.@{prefix-name}-box { background: ~'var(--@{prefix-name}-page-bg-color)'; color: ~'var(--@{prefix-name}-page-text-color)'; }
|
这里的代码是用Less来管理颜色梯度,再用CSS Variable来使用颜色梯度的Less变量,同时语义化来管理不同维度的语义化颜色。CSS Variable更多的用法可以参考这个官方文档。
我们现在有了颜色设计规范和CSS开发规范,最后要面临的问题就是“如何实现组件库的主题方案”了,我下面就用一个最简单的案例来实现一个主题方案,示范一下。
如何实现组件库主题方案?
在讲解组件库的主题方案前,我们先看看效果:

在这张动图中,我实现了一个组件Box,设置了组件语义化的背景色和字体色,切换主题的时候,就用className的优先级来控制语义化的CSS Variable,也就是用新主题的颜色样式覆盖掉原有的默认主题的颜色样式,达到动态切换主题。
具体代码如下述所示。Box组件的Less代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| @prefix-name: my-vue;
@white: #ffffff; @black: #222222;
@gray-4: #bfbfbf;
:root { --@{prefix-name}-page-bg-color: @white; --@{prefix-name}-page-text-color: @black; }
:root { &.@{prefix-name}-theme-dark { --@{prefix-name}-page-bg-color: @black; --@{prefix-name}-page-text-color: @white; } }
.@{prefix-name}-box { background: ~'var(--@{prefix-name}-page-bg-color)'; color: ~'var(--@{prefix-name}-page-text-color)'; }
|
Box组件的Vue代码:
1 2 3 4 5 6 7 8 9 10
| <template> <div :class="{ [baseClassName]: true }"> <slot v-if="$slots.default"></slot> </div> </template>
<script setup lang="ts"> import { prefixName } from '../theme/index'; const baseClassName = `${prefixName}-box`; </script>
|
Box组件的使用代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <template> <Box class="example"> <div :style="{ padding: 10, fontSize: 24 }">这是一个主题演示案例</div> <button @click="onClick">点击换主题色</button> </Box> </template>
<script setup lang="ts"> import { Box } from '../src'; import { prefixName } from '../src/theme/index';
const onClick = () => { const darkThemeName = `${prefixName}-theme-dark`; const html = document.querySelector('html'); if (html?.classList.contains(darkThemeName)) { html?.classList.remove(darkThemeName); } else { html?.classList.add(darkThemeName); } }; </script>
<style> html, body { height: 100%; width: 100%; } .example { height: 100%; padding: 100px; box-sizing: border-box; text-align: center; } </style>
|
通过上述“明亮主题”和“暗黑主题”的切换代码和演示案例,你会发现,组件库的主题方案设计核心就是颜色规范 + CSS Variable控制。是不是很简单?
其实,理论上是可以这么简单理解的,但是实际在组件库的每个组件开发过程中,我们要做好组件内部的主题方案的实现,还是有点“复杂度”的。
这个“复杂度”体现在不同组件的使用场景不同,不同组件的主题方案都要“因地制宜”来实现。具体情况,我给你演示一个基础的组件实现,你就知道了。接下来我就给你演示一下,如何开发一个常见的多状态的按钮组件。
如何开发一个多状态的按钮组件?
在开发组件前,还是先演示一下最终的效果:

上面动图中,演示的是常见组件库里实现的按钮组件,具备多种组件的状态维度,这里维度分成按钮类型、按钮变种(种类)和按钮禁用状态。其中按钮类型有 Default、Primary,Success、Warn和Danger这五类,按钮变种有Contented和Outlined两类,按钮禁用状态有Enabled和Disabled两类。
总的来讲,上述按钮有这三种维度状态,也就是类型、变种和是否禁用。那么,如何实现这个三种维度的叠加管理呢?分解成这三个步骤:
- 第一步,基础按钮组件样式的开发;
- 第二步,实现按钮不同维度组合的样式;
- 第三步,组件的使用状态叠加。
第一步,基础按钮组件的样式开发,也就是实现一个按钮的“底座”,后续可以基于这个底座做各类维度叠加的样式开发,具体代码如下述所示。
Less代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| .@{prefix-name}-button { position: relative; display: inline-block; font-weight: 400; white-space: nowrap; text-align: center; cursor: pointer; user-select: none; touch-action: manipulation; height: 32px; padding: 4px 15px; font-size: 14px; border-radius: 2px; box-sizing: border-box; border-width: 1px; }
|
注意了,我这里将所有颜色梯度规范代码全部放了在本节课源码里的 ./packages/components/theme/variable.less 文件中管理,也用了同样的 variable.less 文件来管理CSS的公共命名前缀。
Vue代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <template> <button :class="{ [baseClassName]: true, }" > <slot v-if="$slots.default"></slot> </button> </template>
<script setup lang="ts"> import { prefixName } from '../theme/index'; const baseClassName = `${prefixName}-button`; </script>
|
这里也要注意,我这里将className的公共命名前缀放在 ./packages/components/theme/index.ts 中管理,前缀名称跟 ./packages/components/theme/variable.less 里保持一致,方便后续统一更换。
**第二步,实现按钮不同维度组合的样式,也就是我们要根据不同状态维度的组合来实现样式的叠加。**我先将按钮的不同维度的样式的className做个统一的管理,其中按钮类型和按钮变种统一管理,形成 5 x 3 的15个基础按钮样式。
具体CSS Variable语义化,如下面所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| :root {
--@{prefix-name}-btn-default-contained-color: @gray-1; --@{prefix-name}-btn-default-contained-border-color: @gray-6; --@{prefix-name}-btn-default-contained-bg-color: @gray-6;
--@{prefix-name}-btn-primary-contained-color: @blue-1; --@{prefix-name}-btn-primary-contained-border-color: @blue-6; --@{prefix-name}-btn-primary-contained-bg-color: @blue-6;
--@{prefix-name}-btn-success-contained-color: @green-1; --@{prefix-name}-btn-success-contained-border-color: @green-6; --@{prefix-name}-btn-success-contained-bg-color: @green-6;
--@{prefix-name}-btn-warning-contained-color: @gold-1; --@{prefix-name}-btn-warning-contained-border-color: @gold-6; --@{prefix-name}-btn-warning-contained-bg-color: @gold-6;
--@{prefix-name}-btn-danger-contained-color: @red-1; --@{prefix-name}-btn-danger-contained-border-color: @red-6; --@{prefix-name}-btn-danger-contained-bg-color: @red-6;
--@{prefix-name}-btn-default-outlined-color: @gray-6; --@{prefix-name}-btn-default-outlined-border-color: @gray-6; --@{prefix-name}-btn-default-outlined-bg-color: @gray-1;
--@{prefix-name}-btn-primary-outlined-color: @blue-6; --@{prefix-name}-btn-primary-outlined-border-color: @blue-6; --@{prefix-name}-btn-primary-outlined-bg-color: @blue-1;
--@{prefix-name}-btn-success-outlined-color: @green-6; --@{prefix-name}-btn-success-outlined-border-color: @green-6; --@{prefix-name}-btn-success-outlined-bg-color: @green-1;
--@{prefix-name}-btn-warning-outlined-color: @gold-6; --@{prefix-name}-btn-warning-outlined-border-color: @gold-6; --@{prefix-name}-btn-warning-outlined-bg-color: @gold-1;
--@{prefix-name}-btn-danger-outlined-color: @red-6; --@{prefix-name}-btn-danger-outlined-border-color: @red-6; --@{prefix-name}-btn-danger-outlined-bg-color: @red-1; }
|
具体className组合实现如下述所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| @import '../../theme/variable.less';
.@{prefix-name}-button {
&.@{prefix-name}-button-default-contained { background: ~'var(--@{prefix-name}-btn-default-contained-bg-color)'; color: ~'var(--@{prefix-name}-btn-default-contained-color)'; border: 1px solid ~'var(--@{prefix-name}-btn-default-contained-border-color)'; } &.@{prefix-name}-button-primary-contained { background: ~'var(--@{prefix-name}-btn-primary-contained-bg-color)'; color: ~'var(--@{prefix-name}-btn-primary-contained-color)'; border: 1px solid ~'var(--@{prefix-name}-btn-primary-contained-border-color)'; } &.@{prefix-name}-button-success-contained { background: ~'var(--@{prefix-name}-btn-success-contained-bg-color)'; color: ~'var(--@{prefix-name}-btn-success-contained-color)'; border: 1px solid ~'var(--@{prefix-name}-btn-success-contained-border-color)'; } &.@{prefix-name}-button-warning-contained { background: ~'var(--@{prefix-name}-btn-warning-contained-bg-color)'; color: ~'var(--@{prefix-name}-btn-warning-contained-color)'; border: 1px solid ~'var(--@{prefix-name}-btn-warning-contained-border-color)'; } &.@{prefix-name}-button-danger-contained { background: ~'var(--@{prefix-name}-btn-danger-contained-bg-color)'; color: ~'var(--@{prefix-name}-btn-danger-contained-color)'; border: 1px solid ~'var(--@{prefix-name}-btn-danger-contained-border-color)'; } &.@{prefix-name}-button-default-outlined { background: ~'var(--@{prefix-name}-btn-default-outlined-bg-color)'; color: ~'var(--@{prefix-name}-btn-default-outlined-color)'; border: 1px solid ~'var(--@{prefix-name}-btn-default-outlined-border-color)'; } &.@{prefix-name}-button-primary-outlined { background: ~'var(--@{prefix-name}-btn-primary-outlined-bg-color)'; color: ~'var(--@{prefix-name}-btn-primary-outlined-color)'; border: 1px solid ~'var(--@{prefix-name}-btn-primary-outlined-border-color)'; } &.@{prefix-name}-button-success-outlined { background: ~'var(--@{prefix-name}-btn-success-outlined-bg-color)'; color: ~'var(--@{prefix-name}-btn-success-outlined-color)'; border: 1px solid ~'var(--@{prefix-name}-btn-success-outlined-border-color)'; } &.@{prefix-name}-button-warning-outlined { background: ~'var(--@{prefix-name}-btn-warning-outlined-bg-color)'; color: ~'var(--@{prefix-name}-btn-warning-outlined-color)'; border: 1px solid ~'var(--@{prefix-name}-btn-warning-outlined-border-color)'; } &.@{prefix-name}-button-danger-outlined { background: ~'var(--@{prefix-name}-btn-danger-outlined-bg-color)'; color: ~'var(--@{prefix-name}-btn-danger-outlined-color)'; border: 1px solid ~'var(--@{prefix-name}-btn-danger-outlined-border-color)'; } }
|
Vue代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <template> <button :class="{ [baseClassName]: true, [btnClassName]: true }" > <slot v-if="$slots.default"></slot> </button> </template>
<script setup lang="ts"> import { prefixName } from '../theme/index'; import type { ButtonType, ButtonVariant } from './types'; const props = withDefaults( defineProps<{ type?: ButtonType; variant?: ButtonVariant; }>(), { type: 'default', variant: 'contained', disabled: false } );
const baseClassName = `${prefixName}-button`; const btnClassName = `${baseClassName}-${props.type}-${props.variant}`; </script>
|
实现后的效果如下:

接下来实现按钮禁用的样式,主要也是通过添加className,利用其添加在后面,优先级更高来覆盖样式,具体实现代码如下:
Less代码:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @import '../../theme/variable.less';
.@{prefix-name}-button {
&.@{prefix-name}-button-disabled { opacity: 0.5; cursor: not-allowed; &:hover { opacity: 0.5; } } }
|
Vue代码改造后如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <template> <button :class="{ [baseClassName]: true, [btnClassName]: true, [disabledClassName]: props.disabled }" > <slot v-if="$slots.default"></slot> </button> </template>
<script setup lang="ts"> import { prefixName } from '../theme/index'; import type { ButtonType, ButtonVariant } from './types'; const props = withDefaults( defineProps<{ type?: ButtonType; variant?: ButtonVariant; disabled?: boolean; }>(), { type: 'default', variant: 'contained', disabled: false } );
const baseClassName = `${prefixName}-button`; const btnClassName = `${baseClassName}-${props.type}-${props.variant}`; const disabledClassName = `${baseClassName}-disabled`; </script>
|
实现上述代码后,使用该按钮组件枚举所有按钮状态叠加效果如下图所示:

第三步,就是实现按钮其他状态叠加,例如鼠标悬浮时候(Hover)等状态,也是添加对应的CSS Variable,然后在Less里使用对应变量。因为这里实现都是一些重复性的代码操作,所以我就不多讲了,留给你来实现或者你可以后续看本课程的完整源码案例。
到了这一步,我们就已经实现了一个多状态的Vue.js 3.x按钮组件,可以通过传入不同的Props来控制显示不同的状态样式和状态叠加的样式。
讲到这,你是不是已经觉得“复杂度”有所提升了?其实这个按钮组件的复杂度基本就到此为止了,也就是说这个按钮的“基础底座”已经实现了。
我们接下来要做的就是基于已有的“底座”,也就是已经实现好的按钮语义化的className和CSS Variable,来配置主题控制和样式主题的切换效果。
如何对多状态的按钮组件进行主题控制?
通过上述的规范设计阶段,我们应该知道,主题核心就是颜色梯度的控制,我们在处理按钮组件的不同颜色的时候,只是选择某个颜色的某个梯度号。也就是说,当我们想按钮主题风格时候,只需要控制“颜色”和“梯度”就行了,再覆盖对应的CSS Variable,就能实现主题快速切换,不需要关注其他。
我这里具体拆解成两步:
- 第一步,对按钮不同状态维度组合选择对应色板的颜色梯度;
- 第二步,将选好的颜色用新className来覆盖原来的CSS Variable。
刚刚实现按钮组件是默认的“明亮”模式的主题,我现在针对按钮组件按钮的所有维度状态,快速实现一个“暗黑”主题的颜色效果。这里可以直接切换颜色“梯度”,直接从取梯度号的镜像号数,例如“蓝色2号”的就换成“蓝色8号”来替换。
然后按照第二步的操作,全部替换到对应的CSS Variable,根据className优先级操作,来覆盖主题样式,具体代码如下述所示:
明亮主题Less代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| @import "./variable.less";
:root { --@{prefix-name}-btn-default-contained-color: @gray-1; --@{prefix-name}-btn-default-contained-border-color: @gray-6; --@{prefix-name}-btn-default-contained-bg-color: @gray-6; --@{prefix-name}-btn-default-contained-color-hover: @gray-2; --@{prefix-name}-btn-default-contained-border-color-hover: @gray-8; --@{prefix-name}-btn-default-contained-bg-color-hover: @gray-8;
--@{prefix-name}-btn-primary-contained-color: @blue-1; --@{prefix-name}-btn-primary-contained-border-color: @blue-6; --@{prefix-name}-btn-primary-contained-bg-color: @blue-6; --@{prefix-name}-btn-primary-contained-color-hover: @blue-2; --@{prefix-name}-btn-primary-contained-border-color-hover: @blue-8; --@{prefix-name}-btn-primary-contained-bg-color-hover: @blue-8;
--@{prefix-name}-btn-success-contained-color: @green-1; --@{prefix-name}-btn-success-contained-border-color: @green-6; --@{prefix-name}-btn-success-contained-bg-color: @green-6; --@{prefix-name}-btn-success-contained-color-hover: @green-2; --@{prefix-name}-btn-success-contained-border-color-hover: @green-8; --@{prefix-name}-btn-success-contained-bg-color-hover: @green-8;
--@{prefix-name}-btn-warning-contained-color: @gold-1; --@{prefix-name}-btn-warning-contained-border-color: @gold-6; --@{prefix-name}-btn-warning-contained-bg-color: @gold-6; --@{prefix-name}-btn-warning-contained-color-hover: @gold-2; --@{prefix-name}-btn-warning-contained-border-color-hover: @gold-8; --@{prefix-name}-btn-warning-contained-bg-color-hover: @gold-8;
--@{prefix-name}-btn-danger-contained-color: @red-1; --@{prefix-name}-btn-danger-contained-border-color: @red-6; --@{prefix-name}-btn-danger-contained-bg-color: @red-6; --@{prefix-name}-btn-danger-contained-color-hover: @red-2; --@{prefix-name}-btn-danger-contained-border-color-hover: @red-8; --@{prefix-name}-btn-danger-contained-bg-color-hover: @red-8;
--@{prefix-name}-btn-default-outlined-color: @gray-6; --@{prefix-name}-btn-default-outlined-border-color: @gray-6; --@{prefix-name}-btn-default-outlined-bg-color: @gray-1; --@{prefix-name}-btn-default-outlined-color-hover: @gray-8; --@{prefix-name}-btn-default-outlined-border-color-hover: @gray-8; --@{prefix-name}-btn-default-outlined-bg-color-hover: @gray-2;
--@{prefix-name}-btn-primary-outlined-color: @blue-6; --@{prefix-name}-btn-primary-outlined-border-color: @blue-6; --@{prefix-name}-btn-primary-outlined-bg-color: @blue-1; --@{prefix-name}-btn-primary-outlined-color-hover: @blue-8; --@{prefix-name}-btn-primary-outlined-border-color-hover: @blue-8; --@{prefix-name}-btn-primary-outlined-bg-color-hover: @blue-2;
--@{prefix-name}-btn-success-outlined-color: @green-6; --@{prefix-name}-btn-success-outlined-border-color: @green-6; --@{prefix-name}-btn-success-outlined-bg-color: @green-1; --@{prefix-name}-btn-success-outlined-color-hover: @green-8; --@{prefix-name}-btn-success-outlined-border-color-hover: @green-8; --@{prefix-name}-btn-success-outlined-bg-color-hover: @green-2;
--@{prefix-name}-btn-warning-outlined-color: @gold-6; --@{prefix-name}-btn-warning-outlined-border-color: @gold-6; --@{prefix-name}-btn-warning-outlined-bg-color: @gold-1; --@{prefix-name}-btn-warning-outlined-color-hover: @gold-8; --@{prefix-name}-btn-warning-outlined-border-color-hover: @gold-8; --@{prefix-name}-btn-warning-outlined-bg-color-hover: @gold-2;
--@{prefix-name}-btn-danger-outlined-color: @red-6; --@{prefix-name}-btn-danger-outlined-border-color: @red-6; --@{prefix-name}-btn-danger-outlined-bg-color: @red-1; --@{prefix-name}-btn-danger-outlined-color-hover: @red-8; --@{prefix-name}-btn-danger-outlined-border-color-hover: @red-8; --@{prefix-name}-btn-danger-outlined-bg-color-hover: @red-2 }
|
暗黑主题Less代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| @import "../variable.less";
:root { &.@{prefix-name}-theme-dark { --@{prefix-name}-btn-default-contained-color: @gray-9; --@{prefix-name}-btn-default-contained-border-color: @gray-4; --@{prefix-name}-btn-default-contained-bg-color: @gray-4; --@{prefix-name}-btn-default-contained-color-hover: @gray-8; --@{prefix-name}-btn-default-contained-border-color-hover: @gray-2; --@{prefix-name}-btn-default-contained-bg-color-hover: @gray-2;
--@{prefix-name}-btn-primary-contained-color: @blue-9; --@{prefix-name}-btn-primary-contained-border-color: @blue-4; --@{prefix-name}-btn-primary-contained-bg-color: @blue-4; --@{prefix-name}-btn-primary-contained-color-hover: @blue-8; --@{prefix-name}-btn-primary-contained-border-color-hover: @blue-2; --@{prefix-name}-btn-primary-contained-bg-color-hover: @blue-2;
--@{prefix-name}-btn-success-contained-color: @green-9; --@{prefix-name}-btn-success-contained-border-color: @green-4; --@{prefix-name}-btn-success-contained-bg-color: @green-4; --@{prefix-name}-btn-success-contained-color-hover: @green-8; --@{prefix-name}-btn-success-contained-border-color-hover: @green-2; --@{prefix-name}-btn-success-contained-bg-color-hover: @green-2;
--@{prefix-name}-btn-warning-contained-color: @gold-9; --@{prefix-name}-btn-warning-contained-border-color: @gold-4; --@{prefix-name}-btn-warning-contained-bg-color: @gold-4; --@{prefix-name}-btn-warning-contained-color-hover: @gold-8; --@{prefix-name}-btn-warning-contained-border-color-hover: @gold-2; --@{prefix-name}-btn-warning-contained-bg-color-hover: @gold-2;
--@{prefix-name}-btn-danger-contained-color: @red-9; --@{prefix-name}-btn-danger-contained-border-color: @red-4; --@{prefix-name}-btn-danger-contained-bg-color: @red-4; --@{prefix-name}-btn-danger-contained-color-hover: @red-8; --@{prefix-name}-btn-danger-contained-border-color-hover: @red-2; --@{prefix-name}-btn-danger-contained-bg-color-hover: @red-2;
--@{prefix-name}-btn-default-outlined-color: @gray-4; --@{prefix-name}-btn-default-outlined-border-color: @gray-4; --@{prefix-name}-btn-default-outlined-bg-color: @gray-9; --@{prefix-name}-btn-default-outlined-color-hover: @gray-2; --@{prefix-name}-btn-default-outlined-border-color-hover: @gray-2; --@{prefix-name}-btn-default-outlined-bg-color-hover: @gray-8;
--@{prefix-name}-btn-primary-outlined-color: @blue-4; --@{prefix-name}-btn-primary-outlined-border-color: @blue-4; --@{prefix-name}-btn-primary-outlined-bg-color: @blue-9; --@{prefix-name}-btn-primary-outlined-color-hover: @blue-2; --@{prefix-name}-btn-primary-outlined-border-color-hover: @blue-2; --@{prefix-name}-btn-primary-outlined-bg-color-hover: @blue-8;
--@{prefix-name}-btn-success-outlined-color: @green-4; --@{prefix-name}-btn-success-outlined-border-color: @green-4; --@{prefix-name}-btn-success-outlined-bg-color: @green-9; --@{prefix-name}-btn-success-outlined-color-hover: @green-2; --@{prefix-name}-btn-success-outlined-border-color-hover: @green-2; --@{prefix-name}-btn-success-outlined-bg-color-hover: @green-8;
--@{prefix-name}-btn-warning-outlined-color: @gold-4; --@{prefix-name}-btn-warning-outlined-border-color: @gold-4; --@{prefix-name}-btn-warning-outlined-bg-color: @gold-9; --@{prefix-name}-btn-warning-outlined-color-hover: @gold-2; --@{prefix-name}-btn-warning-outlined-border-color-hover: @gold-2; --@{prefix-name}-btn-warning-outlined-bg-color-hover: @gold-8;
--@{prefix-name}-btn-danger-outlined-color: @red-4; --@{prefix-name}-btn-danger-outlined-border-color: @red-4; --@{prefix-name}-btn-danger-outlined-bg-color: @red-9; --@{prefix-name}-btn-danger-outlined-color-hover: @red-2; --@{prefix-name}-btn-danger-outlined-border-color-hover: @red-2; --@{prefix-name}-btn-danger-outlined-bg-color-hover: @red-8;
} }
|
最终效果下述所示:

好了,至此,我们就基于主题方案实现了Vue.js 3.x组件库的一个基础组件——按钮组件的功能和主题效果。
总结
这节课的核心是带你学会Vue.js 3.x自研组件库的主题方案设计,以及结合主题方案来开发一个基础组件,了解组件库主题方案实现的完整流程。
简单来说,组件库的主题方案实现就是三点:
- 梳理组件库用到的基本颜色和对应的颜色梯度,用Less或其他CSS预处理器语言来编写;
- 每个组件通过CSS Variable来控制各种语义化颜色,例如按钮的背景颜色;
- 主题控制是利用CSS Varibale来修改覆盖每个组件里语义化的“颜色”和“梯度号”。
最后我们也通过一个实际的按钮组件,演示了这三点的开发流程,完整实现了一个基础组件按钮组件及主题切换的功能,你可以多动手试一试。
思考题
为什么主题控制只考虑颜色,不考虑组件的尺寸的形状控制呢?
欢迎留言参与讨论,我会在章节末统一点评,期待见到你的身影。下一讲见。