AI 产品配色系统设计
原创
灵阙教研团队
B 基础 进阶 |
约 10 分钟阅读
更新于 2026-02-28 AI 导读
AI 产品配色系统设计 色彩理论、语义化配色、暗黑模式、WCAG 无障碍与 Design Token 架构 一、为什么配色系统是 AI 产品的基础设施 颜色不是装饰,是信息。在 AI 产品中,颜色承载着状态、层级、操作和情感: 状态表达:成功(绿)、错误(红)、警告(黄)、加载中(蓝) 层级区分:主操作 vs 次要操作、已读 vs 未读、激活 vs 禁用 品牌识别:用户在 0.1...
AI 产品配色系统设计
色彩理论、语义化配色、暗黑模式、WCAG 无障碍与 Design Token 架构
一、为什么配色系统是 AI 产品的基础设施
颜色不是装饰,是信息。在 AI 产品中,颜色承载着状态、层级、操作和情感:
- 状态表达:成功(绿)、错误(红)、警告(黄)、加载中(蓝)
- 层级区分:主操作 vs 次要操作、已读 vs 未读、激活 vs 禁用
- 品牌识别:用户在 0.1 秒内通过颜色辨认产品
- 情感基调:冷静专业(蓝灰)、温暖友好(橙黄)、科技感(深紫+青)
配色系统不是"选几个好看的颜色",而是建立一套从语义到像素的映射规则,确保产品在任何场景下都能一致、可用、可访问。
配色系统的层级模型
┌───────────────────────────────────────────────┐
│ L1: Semantic Layer │
│ (success, error, warning, info, ...) │
├───────────────────────────────────────────────┤
│ L2: Palette Layer │
│ (blue-50 ~ blue-900, gray-50 ~ gray-900) │
├───────────────────────────────────────────────┤
│ L3: Primitive Layer │
│ (#1a365d, #2b6cb0, #3182ce, ...) │
└───────────────────────────────────────────────┘
使用方向:代码引用 L1 -> L1 映射到 L2 -> L2 定义为 L3
button.primary -> palette.blue.600 -> #2563eb
二、色彩理论基础
2.1 色彩模型选择
| 模型 | 用途 | 优势 |
|---|---|---|
| HSL | CSS 开发 | 直觉化(色相/饱和度/亮度) |
| OKLCH | 现代 CSS | 感知均匀,适合色板生成 |
| HEX/RGB | 存储和传输 | 通用、兼容性最好 |
| HSB | 设计工具 | Figma/Sketch 原生支持 |
2.2 科学化色板生成
传统的色板生成依赖设计师直觉。现代方法使用感知均匀色彩空间(OKLCH),确保同一色相的不同明度变体看起来"等距"。
// color-palette-generator.ts
interface PaletteStep {
name: string; // e.g., '50', '100', ..., '900'
hex: string;
hsl: { h: number; s: number; l: number };
contrastOnWhite: number;
contrastOnBlack: number;
}
function generatePalette(
baseColor: string,
steps: number = 10,
): PaletteStep[] {
/**
* Generate a 10-step palette from a base color using
* perceptually uniform lightness interpolation.
*
* Steps: 50 (lightest) -> 950 (darkest)
* Base color typically maps to step 500-600.
*/
const base = parseHSL(baseColor);
// Define lightness curve (perceptually uniform)
const lightnessSteps = [
{ name: '50', l: 97 },
{ name: '100', l: 93 },
{ name: '200', l: 85 },
{ name: '300', l: 75 },
{ name: '400', l: 63 },
{ name: '500', l: 50 },
{ name: '600', l: 40 },
{ name: '700', l: 32 },
{ name: '800', l: 24 },
{ name: '900', l: 15 },
];
return lightnessSteps.map(step => {
// Adjust saturation: slightly reduce at extremes
const satAdjust = step.l > 90 || step.l < 20
? 0.7
: step.l > 80 || step.l < 30
? 0.85
: 1.0;
const hsl = {
h: base.h,
s: Math.round(base.s * satAdjust),
l: step.l,
};
const hex = hslToHex(hsl);
return {
name: step.name,
hex,
hsl,
contrastOnWhite: calculateContrastRatio(hex, '#ffffff'),
contrastOnBlack: calculateContrastRatio(hex, '#000000'),
};
});
}
function hslToHex(hsl: { h: number; s: number; l: number }): string {
const { h, s, l } = hsl;
const a = s * Math.min(l, 100 - l) / 100;
const f = (n: number) => {
const k = (n + h / 30) % 12;
const color = l / 100 - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color)
.toString(16)
.padStart(2, '0');
};
return `#${f(0)}${f(8)}${f(4)}`;
}
三、语义化配色系统
3.1 从色板到语义
代码中不应该直接使用色板值(如 blue-600),而应该使用语义化名称(如 primary)。这使得主题切换成为一行配置修改。
// semantic-colors.ts
interface SemanticColors {
// Brand
primary: string; // Main brand action
primaryHover: string; // Hover state
primaryActive: string; // Active/pressed state
primarySubtle: string; // Soft background
// Feedback
success: string;
successSubtle: string;
warning: string;
warningSubtle: string;
error: string;
errorSubtle: string;
info: string;
infoSubtle: string;
// Surface
background: string; // Page background
surface: string; // Card/modal background
surfaceHover: string; // Hover state for interactive surfaces
overlay: string; // Modal backdrop
// Text
textPrimary: string; // Main body text
textSecondary: string; // Supporting text
textDisabled: string; // Disabled state
textInverse: string; // Text on dark backgrounds
textLink: string; // Links
textLinkHover: string;
// Border
border: string; // Default borders
borderFocus: string; // Focus ring
borderError: string; // Error state borders
// AI-specific
aiThinking: string; // Loading/processing indicator
aiResponse: string; // AI-generated content background
aiHighlight: string; // AI suggestions/highlights
streaming: string; // Streaming text cursor color
}
function createLightTheme(palette: {
blue: PaletteStep[];
gray: PaletteStep[];
green: PaletteStep[];
red: PaletteStep[];
yellow: PaletteStep[];
}): SemanticColors {
return {
primary: palette.blue[5].hex, // blue-500
primaryHover: palette.blue[6].hex, // blue-600
primaryActive: palette.blue[7].hex, // blue-700
primarySubtle: palette.blue[0].hex, // blue-50
success: palette.green[5].hex,
successSubtle: palette.green[0].hex,
warning: palette.yellow[4].hex,
warningSubtle: palette.yellow[0].hex,
error: palette.red[5].hex,
errorSubtle: palette.red[0].hex,
info: palette.blue[4].hex,
infoSubtle: palette.blue[0].hex,
background: '#ffffff',
surface: palette.gray[0].hex,
surfaceHover: palette.gray[1].hex,
overlay: 'rgba(0, 0, 0, 0.5)',
textPrimary: palette.gray[8].hex, // gray-800
textSecondary: palette.gray[5].hex, // gray-500
textDisabled: palette.gray[3].hex, // gray-300
textInverse: '#ffffff',
textLink: palette.blue[5].hex,
textLinkHover: palette.blue[6].hex,
border: palette.gray[2].hex,
borderFocus: palette.blue[4].hex,
borderError: palette.red[4].hex,
aiThinking: palette.blue[4].hex,
aiResponse: palette.blue[0].hex,
aiHighlight: palette.yellow[1].hex,
streaming: palette.blue[5].hex,
};
}
四、暗黑模式设计
4.1 暗黑模式不是"反转颜色"
暗黑模式的常见错误是简单地将黑白互换。正确的做法是重新映射整个语义层。
| 属性 | 浅色模式 | 暗黑模式 | 说明 |
|---|---|---|---|
| 背景 | #ffffff | #0f172a | 不用纯黑,用深蓝灰 |
| 表面 | #f8fafc | #1e293b | 卡片/弹窗略浅于背景 |
| 主文本 | #1e293b | #e2e8f0 | 不用纯白,减少眩光 |
| 次文本 | #64748b | #94a3b8 | 保持对比度比例 |
| 主色调 | #2563eb | #60a5fa | 暗黑下用更亮的主色 |
| 错误 | #dc2626 | #f87171 | 暗背景上用更亮的红 |
4.2 双主题 Token 系统
// theme-tokens.ts
type ThemeMode = 'light' | 'dark';
interface ThemeTokens {
mode: ThemeMode;
colors: SemanticColors;
}
function createDarkTheme(palette: typeof lightPalette): SemanticColors {
return {
primary: palette.blue[3].hex, // blue-300 (brighter)
primaryHover: palette.blue[2].hex, // blue-200
primaryActive: palette.blue[4].hex, // blue-400
primarySubtle: '#1e3a5f', // Custom dark blue
success: palette.green[3].hex,
successSubtle: '#1a3a2a',
warning: palette.yellow[3].hex,
warningSubtle: '#3a3520',
error: palette.red[3].hex,
errorSubtle: '#3a1a1a',
info: palette.blue[3].hex,
infoSubtle: '#1a2a3a',
background: '#0f172a', // slate-900
surface: '#1e293b', // slate-800
surfaceHover: '#334155', // slate-700
overlay: 'rgba(0, 0, 0, 0.7)',
textPrimary: '#e2e8f0', // slate-200
textSecondary: '#94a3b8', // slate-400
textDisabled: '#475569', // slate-600
textInverse: '#0f172a',
textLink: palette.blue[3].hex,
textLinkHover: palette.blue[2].hex,
border: '#334155', // slate-700
borderFocus: palette.blue[3].hex,
borderError: palette.red[3].hex,
aiThinking: palette.blue[3].hex,
aiResponse: '#1e3a5f',
aiHighlight: '#3a3520',
streaming: palette.blue[3].hex,
};
}
// CSS Custom Properties export
function exportAsCSSVariables(tokens: SemanticColors): string {
const entries = Object.entries(tokens).map(([key, value]) => {
const cssName = key.replace(/([A-Z])/g, '-$1').toLowerCase();
return ` --color-${cssName}: ${value};`;
});
return `:root {\n${entries.join('\n')}\n}`;
}
function exportAsDarkCSS(tokens: SemanticColors): string {
const entries = Object.entries(tokens).map(([key, value]) => {
const cssName = key.replace(/([A-Z])/g, '-$1').toLowerCase();
return ` --color-${cssName}: ${value};`;
});
return `@media (prefers-color-scheme: dark) {\n :root {\n${entries.join('\n')}\n }\n}`;
}
五、WCAG 无障碍设计
5.1 对比度要求
| 级别 | 正文 (>= 18px bold / 24px) | 小文本 (< 18px bold) | 非文本元素 |
|---|---|---|---|
| AA | 3:1 | 4.5:1 | 3:1 |
| AAA | 4.5:1 | 7:1 | - |
5.2 自动对比度校验
// wcag-validator.ts
function relativeLuminance(hex: string): number {
const rgb = hexToRGB(hex);
const [r, g, b] = [rgb.r, rgb.g, rgb.b].map(c => {
const sRGB = c / 255;
return sRGB <= 0.03928
? sRGB / 12.92
: Math.pow((sRGB + 0.055) / 1.055, 2.4);
});
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
function contrastRatio(color1: string, color2: string): number {
const l1 = relativeLuminance(color1);
const l2 = relativeLuminance(color2);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
interface ContrastReport {
pair: [string, string];
ratio: number;
aa: { normalText: boolean; largeText: boolean };
aaa: { normalText: boolean; largeText: boolean };
}
function auditThemeContrast(
theme: SemanticColors,
): ContrastReport[] {
/**
* Audit all critical color pairings in the theme.
* Returns list of pairings that fail WCAG criteria.
*/
const criticalPairs: [string, string, string, string][] = [
// [name1, color1, name2, color2]
['textPrimary', theme.textPrimary, 'background', theme.background],
['textSecondary', theme.textSecondary, 'background', theme.background],
['textPrimary', theme.textPrimary, 'surface', theme.surface],
['primary', theme.primary, 'background', theme.background],
['error', theme.error, 'background', theme.background],
['success', theme.success, 'background', theme.background],
['textInverse', theme.textInverse, 'primary', theme.primary],
];
return criticalPairs.map(([name1, color1, name2, color2]) => {
const ratio = contrastRatio(color1, color2);
return {
pair: [color1, color2],
ratio,
aa: {
normalText: ratio >= 4.5,
largeText: ratio >= 3,
},
aaa: {
normalText: ratio >= 7,
largeText: ratio >= 4.5,
},
};
});
}
function fixContrast(
foreground: string,
background: string,
targetRatio: number = 4.5,
): string {
/**
* Adjust foreground color to meet target contrast ratio.
* Preserves hue, adjusts lightness.
*/
let hsl = parseHSL(foreground);
const bgLuminance = relativeLuminance(background);
// Direction: darken on light background, lighten on dark
const step = bgLuminance > 0.5 ? -2 : 2;
for (let i = 0; i < 50; i++) {
const hex = hslToHex(hsl);
const ratio = contrastRatio(hex, background);
if (ratio >= targetRatio) return hex;
hsl = { ...hsl, l: Math.max(0, Math.min(100, hsl.l + step)) };
}
// Fallback: return black or white
return bgLuminance > 0.5 ? '#000000' : '#ffffff';
}
六、Design Token 架构
6.1 Token 分发流程
Figma (设计师)
|
v
[Figma Tokens Plugin] -> tokens.json (原始 Token)
|
v
[Style Dictionary] -> 转换为多平台格式
|
├── CSS Variables (Web)
├── Tailwind Config (Tailwind CSS)
├── Swift UIColor (iOS)
├── Android XML (Android)
└── JSON (服务端/PPT 生成)
6.2 Token 结构示例
{
"color": {
"primitive": {
"blue": {
"50": { "value": "#eff6ff" },
"100": { "value": "#dbeafe" },
"200": { "value": "#bfdbfe" },
"500": { "value": "#3b82f6" },
"700": { "value": "#1d4ed8" },
"900": { "value": "#1e3a8a" }
}
},
"semantic": {
"primary": {
"value": "{color.primitive.blue.500}",
"description": "Primary brand color, used for main CTAs"
},
"primary-hover": {
"value": "{color.primitive.blue.600}"
},
"text-primary": {
"value": "{color.primitive.gray.800}",
"darkValue": "{color.primitive.gray.200}"
}
}
},
"spacing": {
"xs": { "value": "4px" },
"sm": { "value": "8px" },
"md": { "value": "16px" },
"lg": { "value": "24px" },
"xl": { "value": "32px" }
},
"typography": {
"heading": {
"fontFamily": { "value": "Inter, system-ui, sans-serif" },
"fontWeight": { "value": "700" },
"lineHeight": { "value": "1.2" }
}
}
}
6.3 Tailwind 集成
// tailwind.config.ts
import type { Config } from 'tailwindcss';
import tokens from './tokens.json';
const config: Config = {
theme: {
extend: {
colors: {
primary: {
DEFAULT: tokens.color.semantic.primary.value,
hover: tokens.color.semantic['primary-hover'].value,
subtle: tokens.color.primitive.blue['50'].value,
},
surface: tokens.color.semantic.surface.value,
// ... map all semantic tokens
},
spacing: {
xs: tokens.spacing.xs.value,
sm: tokens.spacing.sm.value,
md: tokens.spacing.md.value,
lg: tokens.spacing.lg.value,
xl: tokens.spacing.xl.value,
},
},
},
};
export default config;
七、AI 产品的特殊配色需求
AI 交互状态的颜色语义
| 状态 | 推荐颜色 | 动态效果 | 说明 |
|---|---|---|---|
| 等待输入 | 中性灰 | 无 | 不引导、不催促 |
| AI 思考中 | 品牌蓝 | 脉冲/渐变动画 | 表示"正在工作" |
| AI 回复中 | 浅蓝背景 | 流式文字光标 | 区分于用户消息 |
| AI 完成 | 标准背景 | 无 | 过渡回正常状态 |
| AI 错误 | 红色提示 | 无 | 但要提供重试按钮 |
| AI 建议 | 黄色高亮 | 淡入 | 引起注意但不打扰 |
代码示例:AI 消息气泡
/* AI message bubble - distinct from user messages */
.message-ai {
background: var(--color-ai-response); /* 浅蓝 */
border-left: 3px solid var(--color-primary); /* 品牌色标识 */
color: var(--color-text-primary);
}
.message-user {
background: var(--color-surface); /* 中性灰 */
color: var(--color-text-primary);
}
/* AI thinking indicator */
.ai-thinking {
display: inline-flex;
gap: 4px;
}
.ai-thinking .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-ai-thinking);
animation: pulse 1.4s ease-in-out infinite;
}
八、总结
配色系统的最终目标是让颜色"隐形"——用户不会注意到颜色,但会因为颜色而更高效地使用产品。好的配色系统应该:
- 语义化:开发者用
primary而非#2563eb - 可主题化:一行配置切换明暗主题
- 可验证:自动化工具检测对比度和一致性
- 可扩展:新增颜色不破坏现有体系
- 可感知:色板在感知上均匀分布
Maurice | maurice_wen@proton.me