如何编写样式

JeremyJone ... 2025-3-15 大约 7 分钟

# 如何编写样式

想编写好一个组件库的样式内容,可不是简简单单的把每个组件的样式写好就可以了。你需要考虑把样式内容变成一个系统,即样式系统,按照做系统的思维去思考,这样才能更好的编写样式。

# 样式系统

样式系统是一个系统,它包含了一系列的样式内容,这些样式内容是有规律的,是有组织的,是有层次的。这样的样式系统,可以让你更好的去编写样式,更好的去维护样式。

样式系统的内容包括:

  • 基础样式
  • 主题样式
  • 复用样式
  • 工具方法
  • 用户自定义样式
  • 响应式样式 // 这个我们这里暂时不考虑,但需要知道应该怎么做

还有一些其他内容,也是需要我们在编写样式系统的时候考虑的,但这里我们认为它们和响应式一样,暂时不考虑。

# 系统结构

我们这个项目采用 SCSS 来编写。

SCSS 是 Sass 3 引入新的语法,完全兼容 CSS3,并且继承了 Sass 的强大功能。SCSS 是 Sass 3 引入新的语法,完全兼容 CSS3,并且继承了 Sass 的强大功能。

大体结构如下:

├── styles
│   ├── base
│   │   ├── _color.scss   // 颜色
│   │   ├── ...           // 其他基础样式
│   ├── components        // 组件样式
│   │   ├── button.scss  // 按钮
│   │   ├── ...           // 其他组件
│   ├── functions         // 函数
│   ├── transitions       // 过渡
│   ├── vars.scss         // 变量
│   ├── index.scss        // 入口文件
1
2
3
4
5
6
7
8
9
10
11

我们做最简单的示例,真正的项目中,你需要更多的内容。

# 基础样式

我们首先编写变量,这是所有样式的基础。

我们定义最基本的主题颜色变量:

// vars.scss

$base-colors: (
  "primary": #8b7ae5,    // 主色
  "secondary": #c48be0,  // 次色
  "success": #52b898,    // 成功
  "warning": #e6ba69,    // 警告
  "error": #e67373,      // 错误
  "info": #7991b3,       // 信息
  "neutral": #504e5a     // 中性
);
1
2
3
4
5
6
7
8
9
10
11

所有颜色类别与我们之前定义的 button 以及后续组件类型保持一致,这样我们可以在组件中直接使用这些颜色。

们要这里要考虑一个问题,就是我们的样式系统是不是要支持用户自定义样式。如果支持,那么我们需要考虑如何让用户自定义样式。

这里我们参考了 ElementPlus 的做法,使用 default 来实现替换:

// vars.scss

$base-colors: () !default;
$base-colors: map.deep-merge((
  "primary": #8b7ae5,
  "secondary": #c48be0,
  "success": #52b898,
  "warning": #e6ba69,
  "error": #e67373,
  "info": #7991b3,
  "neutral": #504e5a
), $base-colors);
1
2
3
4
5
6
7
8
9
10
11
12

这样做,可以让用户直接覆盖我们的默认颜色。

//如何替换

@forward "@xpyjs/x-ui" with (
    $base-colors: (
        "primary": #ff0000,
        "secondary": #00ff00,
        "success": #0000ff,
        "warning": #ffff00,
        "error": #ff00ff,
        "info": #00ffff,
        "neutral": #ffffff
    )
)
1
2
3
4
5
6
7
8
9
10
11
12
13

这样的写法,可以让用户对每一个具有 !default 的变量进行覆盖。在使用时注意要把 forward 后面的路径替换成正确的路径。比如这里已经正确安装了组件库,那么这么写(@xpyjs/x-ui)就没有问题。

# 其他默认变量

可能我们还需要文本颜色、边框颜色、背景颜色等等,这些都是我们的基础样式。我们参考 $base-colors 的写法实现即可。这里以背景色为例:

// vars.scss

$background-colors: () !default;
$background-colors: map.deep-merge(
  (
    "base": (
      "light": #ffffff,
      "dark": #121212
    ),
    "hover": (
      "light": #f5f5f5,
      "dark": #212121
    ),
    "disabled": (
      "light": #e0e0e0,
      "dark": #333333
    ),
    "mask": (
      "light": rgba(0, 0, 0, 0.15),
      "dark": rgba(255, 255, 255, 0.15)
    )
  ),
  $background-colors
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

对的,写法不限制,但是替换时需要结构保持一致即可。

# 生成标准颜色

有了变量,我们便可以在 base/_color.scss 中生成我们的颜色了。

我们要考虑使用情况。在当前现代化浏览器中,使用 css 变量是一个很不错的选择,所以我们可以生成 css 变量,然后在需要的地方直接使用即可。

CSS 变量的优点:

  • 生成简单,使用方便
  • 可以在运行时修改
  • 可以在媒体查询中使用
// base/_color.scss

@use "sass:map";
@use "sass:color";
@use "../vars" as vars;

// 获取颜色
$base-colors: vars.$base-colors;
$background-colors: vars.$background-colors;
1
2
3
4
5
6
7
8
9

基础配置工作就做好了,下面就可以生成了。

我们把所有的变量声称在 :root 上:

// base/_color.scss

:root {
    @each $name, $color in $base-colors {
        --x-color-#{$name}: #{$color};
    }

    @each $name, $color in $background-colors {
        --x-bg-color-#{$name}: #{map.get($colors, 'light')};
    }
}
1
2
3
4
5
6
7
8
9
10
11

此时我们已经有了最基础的颜色模板,我们可以在任何地方使用像 var(--x-color-primary) 这样的变量。

但是,我们既然是要编写一套颜色系统,那么色阶是必须的。 色阶有很多方式,可以通过函数,还可以适用对象。我们这里使用对象来生成色阶,我认为这样更灵活一些。

// base/_color.scss


// 配置色阶。用于生成不同百分比浓度的主题颜色
$color-mode-steps: (
  "50": (
    percent: 90%,
    color: #ffffff
  ),
  "100": (
    percent: 80%,
    color: #ffffff
  ),
  "200": (
    percent: 60%,
    color: #ffffff
  ),
  "300": (
    percent: 40%,
    color: #ffffff
  ),
  "400": (
    percent: 20%,
    color: #ffffff
  ),
  "500": (
    percent: 0%,
    color: #ffffff
  ),
  "600": (
    percent: 20%,
    color: #000000
  ),
  "700": (
    percent: 40%,
    color: #000000
  ),
  "800": (
    percent: 60%,
    color: #000000
  ),
  "900": (
    percent: 70%,
    color: #000000
  )
);
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

然后我们来生成色阶:

// base/_color.scss

@use "sass:map";
@use "sass:color";

:root {
    // 生成颜色
    @each $name, $color in $base-colors {
        --x-color-#{$name}: #{$color};

        @each $step, $value in $color-mode-steps {
            --x-color-#{$name}-#{$step}: color.mix($color, map.get($value, "color"), map.get($value, "percent"));
        }
    }

    // 生成背景色
    @each $name, $color in $background-colors {
        --x-bg-color-#{$name}: #{map.get($colors, 'light')};
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 主题模式

我们的组件库是支持主题模式的,所以我们需要考虑主题模式下的样式。在不同情况下,我们都要考虑。

目前通用的亮暗主题配置方案:

  • dark class
  • prefers-color-scheme
  • data-theme

我们把这两种方案都适配就可以了。

:root {}
:root.dark {}

[data-theme="dark"] {}
[data-theme="light"] {}

@media (prefers-color-scheme: dark) {}
@media (prefers-color-scheme: light) {}
1
2
3
4
5
6
7
8

我们需要把之前在 :root 上写的内容,都复制一份到这些地方。

为了方便,我们可以使用 mixin 来实现:

@mixin generate-color-variables($is-dark: false) {
    $mode: if($is-dark, "dark", "light");  // 利用变量,来判断是亮色还是暗色

    // 生成颜色
    @each $name, $color in $base-colors {
        --x-color-#{$name}: #{$color};

        @each $step, $value in $color-mode-steps {
            --x-color-#{$name}-#{$step}: color.mix($color, map.get($value, "color"), map.get($value, "percent"));
        }
    }

    // 生成背景色
    @each $name, $color in $background-colors {
        --x-bg-color-#{$name}: #{map.get($colors, $mode)};  // 这里使用了 $mode
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

然后我们在所有地方使用这个 mixin:

:root {
    @include generate-color-variables(false);
}
:root.dark {
    @include generate-color-variables(true);
}

[data-theme="light"] {
    @include generate-color-variables(false);
}
[data-theme="dark"] {
    @include generate-color-variables(true);
}

@media (prefers-color-scheme: light) {
    @include generate-color-variables(false);
}
@media (prefers-color-scheme: dark) {
    :root:not([data-theme]) {  // 这个操作是为了避免无切换时自动生效
        @include generate-color-variables(true);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这样生成的色阶,我们不用考虑 light/dark 对颜色的影响,因为我们已经在生成色阶的时候考虑了。

现在,我们在大体上就完成了所有的基础样式的编写。它目前就有了一套完整的色阶。

# 组件样式

有了像上面的样式系统结构,我们的组件样式就可以完全基于这些样式去编写了。

就比如我们现在要编写的按钮样式,就可以直接使用我们的颜色变量:

// components/button.scss

@use "../vars" as vars;
@use "sass:map";

$types: (
  "default": (
    "bg": --x-color-neutral
  ),
  "primary": (
    "bg": --x-color-primary
  ),
  "secondary": (
    "bg": --x-color-secondary
  ),
  "success": (
    "bg": --x-color-success
  ),
  "warning": (
    "bg": --x-color-warning
  ),
  "error": (
    "bg": --x-color-error
  ),
  "info": (
    "bg": --x-color-info
  )
)

@each $type, $value in $types {
    .x-button-#{$type} {
        background-color: var(#{map.get($value, "bg")});
    }
}
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

这样就通过我们之前定义的变量,来完成背景颜色的设置。

为什么要用 --x-color-primary 这样的变量名,而不是直接使用 var(--x-color-primary) 这样的变量值。是因为我们可以更加灵活的改变它们。

@each $type, $value in $types {
    .x-button-#{$type} {
        background-color: var(#{map.get($value, "bg")});

        // 这样通过尾值来改变颜色
        &:hover {
            background-color: var(#{map.get($value, "bg")}-100);
        }

        &:active {
            background-color: var(#{map.get($value, "bg")}-200);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14