
别再瞎找了!颜色系统速查手册,5分钟搞定项目配色
看了一堆教程还是不会写项目?别急,问题往往出在“颜色”这个看似简单实则坑爹的细节上。很多后端转全栈,或者前端新手,一上来就硬编码 #FF0000,结果项目换皮难如登天,维护成本直接爆炸。
今天这篇颜色系统速查手册,不聊玄学审美,只讲工程化落地。我们将围绕【的颜色】管理,从零搭建一套可复现、易扩展的颜色架构。无论你是做管理后台还是 C 端应用,这套方案都能让你的代码清爽十倍。
项目目标:告别硬编码的混乱时代
在正式写代码前,我们必须明确一个核心痛点:颜色不应是散落在 CSS 里的魔法数字,而应是配置化的资产。
传统开发中,你可能见过这样的场景:设计稿给的是 RGB 值,你手动换算成 Hex 填进 CSS。
产品经理说“主色调稍微深一点”,你要全局搜索替换,漏改一处就是线上事故。
深色模式适配时,你发现颜色变量根本没抽离,只能重新写一套 CSS。我们要搭建的项目目标是:单一数据源:所有颜色定义在一个 JS/TS 文件中,其他文件只引用变量。
自动化生成:基于基础色,自动派生出 hover、active、disabled 等状态色。
主题化支持:通过切换 CSS 变量,一键实现亮/暗模式切换。这不仅仅是一个配色方案,而是一套颜色工程化标准。就像我们遵循 RFC 规范编写网络协议一样,颜色也需要遵循严格的生成逻辑,确保在任何设备、任何屏幕下,视觉体验的一致性。
目录结构:工程化的第一步
为了体现“可复现”的工程化思维,我们的项目结构必须清晰。不要把所有东西都塞进 App.tsx,那是在给未来的自己挖坑。
推荐如下目录结构(以 React + TypeScript 为例):
src/
├── styles/
│ ├── tokens/
│ │ └── colors.ts # 核心:颜色令牌定义
│ ├── themes/
│ │ ├── light.ts # 亮色主题映射
│ │ └── dark.ts # 暗色主题映射
│ └── global.css # 全局 CSS 变量注入
├── utils/
│ └── color.ts # 颜色处理工具函数
└── components/└── Button.tsx # 示例组件,引用颜色关键点解析:tokens/colors.ts 是“宪法”,定义了所有颜色的原始值和派生规则。
themes/ 是“执行层”,将令牌映射到具体的 CSS 变量名。
utils/color.ts 是“工具库”,提供 HSL 转 Hex、亮度计算等底层能力。这种分层结构,让设计同学只需要改 tokens,前端同学只需要改 themes,业务代码完全无感。这就是工程化的魅力:解耦。
核心代码实现:从理论到代码
1. 定义颜色令牌 (tokens/colors.ts)
颜色不是随便选的,要有体系。我们采用 HSL(色相、饱和度、亮度)模型,因为它更符合人眼对颜色的感知,且便于程序化调整。
// src/styles/tokens/colors.tsexport interface ColorToken {/** 基础色相 (0-360) */hue: number;/** 基础饱和度 (0-100) */saturation: number;/** 基础亮度 (0-100) */lightness: number;
}/*** 主色调定义* 这里我们可以引用设计系统的标准色值*/
export const primary: ColorToken = {hue: 217, // 蓝色saturation: 91,lightness: 60
};/*** 功能色定义*/
export const success: ColorToken = {hue: 142, // 绿色saturation: 71,lightness: 45
};export const warning: ColorToken = {hue: 38, // 橙色saturation: 92,lightness: 50
};export const danger: ColorToken = {hue: 0, // 红色saturation: 84,lightness: 60
};/*** 中性色(灰阶)* 基于 HSL 生成,确保在不同背景下对比度符合 WCAG 标准*/
export const neutral = {gray100: { hue: 210, saturation: 40, lightness: 96 },gray500: { hue: 210, saturation: 10, lightness: 55 },gray900: { hue: 210, saturation: 20, lightness: 15 },
};逐行讲解:我们定义了 ColorToken 接口,强制规范颜色数据结构。
primary 等变量存储的是 HSL 三元组,而不是最终的 CSS 字符串。为什么?因为我们需要在运行时根据主题动态调整 lightness。2. 颜色处理工具 (utils/color.ts)
这是整个系统的“大脑”。我们需要两个核心功能:HSL 转 CSS 字符串 和 亮度派生。
// src/utils/color.tsimport { ColorToken } from '../styles/tokens/colors';/*** 将 HSL 对象转换为 CSS hsl() 字符串* 参考: MDN Web Docs - hsl()*/
export const hslToCss = (token: ColorToken, lightnessAdjust: number = 0): string = {const l = Math.max(0, Math.min(100, token.lightness + lightnessAdjust));return `hsl(${token.hue}, ${token.saturation}%, ${l}%)`;
};/*** 生成一组派生颜色* @param base 基础颜色* @returns 包含 hover, active, focus 等状态的对象*/
export const generateColorScale = (base: ColorToken) = {return {default: hslToCss(base),// Hover 状态:亮度增加 10%hover: hslToCss(base, 10),// Active 状态:亮度减少 10%active: hslToCss(base, -10),// Focus 状态:保持默认,但增加轮廓色(通常在 CSS 中处理)focus: hslToCss(base),// Disabled 状态:降低饱和度,增加亮度disabled: hslToCss({ ...base, saturation: base.saturation * 0.5, lightness: 80 }),};
};避坑指南:注意 Math.max 和 Math.min 的边界处理。HSL 的亮度超出 0-100 会导致浏览器渲染异常或颜色溢出,这是新手最容易忽略的 Bug。
disabled 状态的处理逻辑:不是简单加灰,而是降低饱和度并提高亮度,这样视觉上更“轻”,符合交互直觉。3. 主题映射与 CSS 变量注入 (themes global.css)
现在,我们将令牌映射到 CSS 变量。这是实现“一键换肤”的关键。
// src/styles/themes/light.tsimport { primary, success, warning, danger, neutral } from '../tokens/colors';
import { generateColorScale } from '../../utils/color';export const lightTheme = {'--color-primary-default': generateColorScale(primary).default,'--color-primary-hover': generateColorScale(primary).hover,'--color-primary-active': generateColorScale(primary).active,'--color-success': generateColorScale(success).default,'--color-warning': generateColorScale(warning).default,'--color-danger': generateColorScale(danger).default,'--color-bg-body': hslToCss(neutral.gray100),'--color-text-primary': hslToCss(neutral.gray900),
};/* src/styles/global.css */:root {/* 这里的值将由 JS 动态注入,此处仅为占位或默认值 */--color-primary-default: #3b82f6;--color-primary-hover: #60a5fa;--color-text-primary: #1e293b;
}body {background-color: var(--color-bg-body);color: var(--color-text-primary);transition: background-color 0.3s ease, color 0.3s ease;
}工程化细节:在 App.tsx 或入口文件中,我们需要根据当前主题(light/dark)将 lightTheme 对象注入到 document.documentElement.style 中。
这样,当用户切换主题时,JS 更新 CSS 变量,浏览器自动重新计算所有依赖该变量的元素样式,无需重新渲染 DOM。性能极高。运行与测试:验证你的颜色系统
代码写完只是第一步,验证才是工程师的素养。
1. 单元测试 (Jest)
我们需要测试 generateColorScale 的正确性。
// src/utils/__tests__/color.test.tsimport { hslToCss, generateColorScale } from '../color';
import { primary } from '../../styles/tokens/colors';describe('Color Utils', () = {test('hslToCss should return valid css string', () = {const result = hslToCss(primary);expect(result).toBe('hsl(217, 91%, 60%)');});test('generateColorScale should adjust lightness correctly', () = {const scale = generateColorScale(primary);// 默认亮度 60%expect(scale.default).toBe('hsl(217, 91%, 60%)');// Hover 亮度 70%expect(scale.hover).toBe('hsl(217, 91%, 70%)');// Active 亮度 50%expect(scale.active).toBe('hsl(217, 91%, 50%)');});
});2. 视觉回归测试
颜色是视觉资产,必须保证视觉一致性。推荐引入 Percy 或 Chromatic 进行视觉回归测试。配置一个测试页面,展示所有颜色色块。
当 tokens 或 themes 发生变化时,CI 流程会自动截图并对比,如果有像素级差异,则阻断合并。3. 可访问性检查 (A11y)
这是很多开发者忽略的红线。文本颜色与背景颜色的对比度必须达到 WCAG 2.1 AA 标准(正文至少 4.5:1,大文本至少 3:1)。
使用 axe-core 或浏览器插件检查。
避坑:不要只靠颜色传达信息(如仅用红色表示错误),必须配合图标或文字。这不仅是规范,更是对色盲用户的尊重。优化扩展:进阶技巧与避坑
1. 动态颜色计算
如果你的应用需要根据用户选择动态改变主题色(如“选择你喜欢的颜色”),可以利用 Canvas API 实时计算对比度,自动调整文本颜色(黑/白),确保可读性。
// 简单示例:根据背景亮度决定文字颜色
export const getTextColor = (bgLightness: number) = {return bgLightness 50 ? 'black' : 'white';
};2. 与构建工具集成
在 Vite 或 Webpack 中,可以使用 postcss-preset-env 等插件,将 HSL 值在构建时预计算为 Hex 或 RGB,减少运行时开销。注意:如果使用了 CSS 变量动态切换,则不要预计算,否则动态切换会失效。这是一个权衡(Trade-off):静态构建快 vs 动态灵活。3. 设计系统对接
如果你的团队有设计师,建议将 tokens/colors.ts 与 Figma 的 Variables 或 Tokens Studio 插件同步。设计师在 Figma 改色值 - 导出 JSON - 脚本自动更新 tokens/colors.ts - Git 提交 - CI 部署。
这种 Design-to-Code 的自动化流程,是大型项目保持视觉一致性的唯一途径。小结:工程化思维的延伸
回顾整个项目,我们并没有写多少复杂的业务逻辑,而是建立了一套颜色基础设施。抽象:将颜色抽象为 HSL 令牌,而非魔法数字。
派生:通过算法自动生成状态色,减少人工错误。
映射:通过 CSS 变量实现主题与逻辑的解耦。
验证:通过单测和视觉回归确保质量。这套颜色系统速查手册的核心思想,其实可以迁移到很多领域:图标管理、间距系统、圆角规范。工程化的本质,就是将重复的、易错的工作,转化为标准化的、可配置的规则。
不要觉得颜色只是前端的小事。在一个大型系统中,颜色的一致性直接影响用户体验和品牌认知。正如我们在网络开发中严格遵循 RFC 规范 以确保数据包的准确传输一样,我们在 UI 开发中严格遵循颜色令牌规范,是为了确保视觉信息的准确传达。
你更常用哪种写法?是直接写 Hex 值图省事,还是像我们这样搭建一套完整的颜色系统?评论区交流一下你的实践经历,看看有没有更骚的操作。