第 6 章:CSS 多色调系统
如果你只读 sag-icons 的一章,应该读这一章。
CSS 多色调系统是 sag-icons 的灵魂。它实现了 sag-icons 的标志性特性——"加个 is-active 类就变彩色"。但背后其实是一套通用的 CSS 主题系统设计模式,可以套到任何 UI 库里。
读完这一章你会知道:
- CSS 变量 +
data-* 属性怎么组合做主题
is-active 状态切换的完整链路
- 运行时注入 CSS 的去重机制
- 怎么给用户提供"自己引 CSS"的入口
6.1 核心设计:CSS 变量 + data-tone
sag-icons 的 CSS 全文:
src/css/svg-icon.ts
export const svgIconCss = `:root {
--sag-icon-blue: #3b82f6;
--sag-icon-cyan: #06b6d4;
--sag-icon-green: #22c55e;
--sag-icon-amber: #f59e0b;
--sag-icon-white: #ffffff;
/* 默认灰色(非激活态) */
--sag-icon-default: #9ca3af;
}
sag-icon {
display: inline-block;
vertical-align: middle;
width: 1.25rem;
height: 1.25rem;
color: var(--sag-icon-default);
flex-shrink: 0;
transition: color 160ms ease, filter 160ms ease;
}
sag-icon > svg {
width: 100%;
height: 100%;
display: block;
}
sag-icon [data-tone] {
color: currentColor;
opacity: 0.8;
transition: color 180ms ease, opacity 180ms ease;
}
/* 激活态:彩色 */
sag-icon.is-active {
filter: saturate(1.08);
}
sag-icon.is-active [data-tone] {
opacity: 1;
}
sag-icon.is-active [data-tone="blue"] {
color: var(--sag-icon-blue);
}
sag-icon.is-active [data-tone="cyan"] {
color: var(--sag-icon-cyan);
}
sag-icon.is-active [data-tone="green"] {
color: var(--sag-icon-green);
}
sag-icon.is-active [data-tone="amber"] {
color: var(--sag-icon-amber);
}
sag-icon.is-active [data-tone="white"] {
color: var(--sag-icon-white);
}`;
对应文件:src/css/svg-icon.ts
我们一段段拆。
6.2 第一层:CSS 变量定义
:root {
--sag-icon-blue: #3b82f6;
--sag-icon-cyan: #06b6d4;
--sag-icon-green: #22c55e;
--sag-icon-amber: #f59e0b;
--sag-icon-white: #ffffff;
--sag-icon-default: #9ca3af;
}
把所有颜色集中到 :root。用户换肤只要覆盖 6 个变量:
:root {
--sag-icon-blue: #5856eb; /* 自定义品牌色 */
--sag-icon-default: #d4d4d8;
}
不需要 props、不需要 wrapper、CSS 即真相。
为什么用--sag-icon- 前缀
所有变量都加 sag-icon- 前缀,避免和用户变量冲突。这是 CSS 变量命名的常识:
- 库名 / 包名作为前缀
- 颜色语义名(blue/green/amber)作为后缀
6.3 第二层:sag-icon 默认态
sag-icon {
display: inline-block;
vertical-align: middle;
width: 1.25rem;
height: 1.25rem;
color: var(--sag-icon-default);
flex-shrink: 0;
transition: color 160ms ease, filter 160ms ease;
}
sag-icon > svg {
width: 100%;
height: 100%;
display: block;
}
关键点:
color: var(--sag-icon-default) — 默认灰色。color 是 CSS 里一个特殊属性:它能被 currentColor 关键字继承
vertical-align: middle — 让图标和文字基线对齐
flex-shrink: 0 — 在 flex 容器里不被压缩
transition — 颜色切换带过渡
注意:所有 path 用 fill="currentColor"(上一章讲过),所以 color 变化会传到 SVG 内部。这就是 CSS 主题能起作用的关键。
6.4 第三层:data-tone 的通用处理
sag-icon [data-tone] {
color: currentColor;
opacity: 0.8;
transition: color 180ms ease, opacity 180ms ease;
}
这一段是关键中的关键。
默认态下,所有带 data-tone 的 path:
color: currentColor — 继承 <sag-icon> 的 color(默认灰色)
opacity: 0.8 — 比"白板"状态略弱一点
为什么是 0.8?给用户一个"激活前是次要状态、激活后是主状态"的层次感。如果默认就是 1.0,激活态没什么视觉变化。
6.5 第四层:is-active 激活态
sag-icon.is-active {
filter: saturate(1.08);
}
sag-icon.is-active [data-tone] {
opacity: 1;
}
sag-icon.is-active [data-tone="blue"] {
color: var(--sag-icon-blue);
}
/* ...其他色... */
加 is-active 类后:
- 整个图标
filter: saturate(1.08) — 颜色饱和度 +8%(微妙的"被点亮"感)
- 所有 data-tone 元素
opacity: 1 — 去掉默认的"次要"感
- 每个具体 data-tone 应用对应的 CSS 变量
这就是"加个类就彩色"的全部秘密——一组 CSS 规则。
为什么用[data-tone="blue"] 而不是.blue
data-tone 是 HTML5 标准属性,对自定义元素友好:
- HTML 自带,不需要 BEM 命名空间
- 选择器
[data-tone="blue"] 性能 OK(现代浏览器高度优化)
- SVG path 里也能用(
<path data-tone="blue"> 是合法 SVG)
CSS 优先级
注意选择器的特异性:
sag-icon [data-tone="blue"] ← 默认 0,1,1
sag-icon.is-active [data-tone="blue"] ← 激活态 0,2,1
激活态选择器多一个 .is-active 类,特异性更高。所以 is-active 类加上后样式自动生效,不需要 !important。
6.6 视觉链路:颜色切换的完整流程
一个完整的"从默认到激活"颜色切换,经历了 5 个步骤:
1. 用户加类:<sag-icon name="workbench" class="is-active" />
2. CSS 选择器匹配:sag-icon.is-active [data-tone="blue"] 命中
3. color: var(--sag-icon-blue) 把 blue 元素的 color 切到 #3b82f6
4. <path data-tone="blue" fill="currentColor"> 用 currentColor 拿值
5. 浏览器过渡:transition: color 180ms ease 让切换平滑
每一步都是纯 CSS,没有任何 JS 介入。这就是 sag-icons"加个类就完事"的本质。
6.7 运行时注入 CSS
sag-icons 在 src/index.ts 里自动把 CSS 字符串塞进 <head>:
src/index.ts
import { svgIconCss } from "./css/svg-icon";
const STYLE_ID = "sag-icons-styles";
function injectStyles(): void {
if (typeof document === "undefined") return;
if (document.getElementById(STYLE_ID)) return;
const styleEl = document.createElement("style");
styleEl.id = STYLE_ID;
styleEl.textContent = svgIconCss;
document.head.appendChild(styleEl);
}
injectStyles();
对应文件:src/index.ts
几个关键点:
SSR 安全
if (typeof document === "undefined") return;
Node.js / SSR 环境(Next.js / Nuxt / SvelteKit)下 document 不存在。直接操作会抛错。
去重
if (document.getElementById(STYLE_ID)) return;
每次 import "@jujiuyey/sag-icons" 都会跑 injectStyles()。但同一页面 import 多次只应该注入一次。
检查 STYLE_ID = "sag-icons-styles" 的 <style> 元素是否存在:
- 存在 → 已经注入了,直接返回
- 不存在 → 创建并注入
这是幂等性的具体实现。
为什么用id 而不是 querySelectorAll
理论上可以用 document.querySelectorAll('style[data-sag-icons]') 之类的检查。但 id 是最快、最简单的:
if (document.getElementById("sag-icons-styles")) return;
O(1) 查询,浏览器内部优化好。
6.8 单独 CSS 入口
sag-icons 的 CSS 默认运行时注入——用户 import "@jujiuyey/sag-icons" 就完事。
但有些场景用户想自己控制 CSS 引入。比如:
- 公司项目要求所有样式走 PostCSS 处理
- 想用
<link rel="stylesheet"> 而不是 <style> 注入
- 想在文档
<head> 顶部放 CSS 而不是在 JS 执行后注入
sag-icons 提供了原始 CSS 文件:
package.json (假设)
"exports": {
".": { "...": "..." },
"./css/svg-icon.css": "./dist/svg-icon.css"
}
用户可以:
import "@jujiuyey/sag-icons"; // 注册 Web Component
import "@jujiuyey/sag-icons/css/svg-icon.css"; // 单独引 CSS
第一行只注册 <sag-icon>,不注入 CSS(因为 injectStyles 还是会跑——但 STYLE_ID 没冲突也无害)。
注:sag-icons 当前 vite.config.ts 没配置 CSS 产物。这是已知的小坑,第 8 章会讲怎么补。
6.9 为什么不用 Shadow DOM
Web Components 的三大特性:Custom Elements + Shadow DOM + HTML Templates。
sag-icons 故意没用 Shadow DOM。原因:
/* 这段 CSS 在 Shadow DOM 外面 */
:root {
--sag-icon-blue: #3b82f6;
}
/* 内部 path 怎么拿到这个变量? */
sag-icon.is-active [data-tone="blue"] {
color: var(--sag-icon-blue); /* ← 拿不到 */
}
Shadow DOM 的核心是样式隔离——外部 CSS 进不去,内部 CSS 出不来。
但 sag-icons 的 CSS 主题系统依赖外部变量穿透:
/* 用户在外部写 */
:root {
--sag-icon-blue: #5856eb; /* 自定义品牌色 */
}
/* sag-icon 内部的 path 用这个变量 */
sag-icon.is-active [data-tone="blue"] {
color: var(--sag-icon-blue);
}
这必须没有 Shadow DOM。CSS 变量通过 DOM 树继承穿透进去。
不用 Shadow DOM 的代价
- 全局样式污染:用户写的
sag-icon { ... } 会影响 sag-icons(反过来也是)
- sag-icons 内部样式必须用
sag-icon [data-tone] 这种足够具体的选择器
但 sag-icons 的 CSS 只有 ~40 行,污染风险可控。
6.10 怎么扩展:hover / focus / disabled
is-active 不是唯一的扩展点。CSS 多色调系统可以横向扩展:
/* hover 态 */
sag-icon:hover:not(.is-active) [data-tone] {
opacity: 0.9;
color: var(--sag-icon-cyan); /* 给个轻提示 */
}
/* focus 态 */
sag-icon:focus-visible {
outline: 2px solid var(--sag-icon-blue);
outline-offset: 2px;
}
/* disabled 态 */
sag-icon[disabled],
sag-icon.is-disabled {
opacity: 0.5;
cursor: not-allowed;
}
sag-icons 本身没实现这些,但 CSS 系统设计好了,加这些就是几行 CSS。
这就是 CSS 变量 + 属性驱动的扩展性。
6.11 这一章的产出
到这里你应该能:
- 解释 sag-icons 的 CSS 主题系统怎么工作
- 解释
is-active 类激活态的 CSS 选择器链路
- 解释 CSS 运行时注入的去重机制
- 解释为什么不用 Shadow DOM
下一章讲类型系统——怎么让 <sag-icon name="..." /> 有 TS 自动补全,怎么做按需导入的子路径。
对应文件
| 内容 | 路径 |
|---|
| CSS 字符串 | src/css/svg-icon.ts |
| 注入逻辑 | src/index.ts:10-23 |
| 图标 data-tone 标记 | src/data/icons/<category>/<name>.ts |