设计系统多品牌架构实践一套 Token 驱动多套视觉的系统化方案一、多品牌视觉统一的管理困局Token 重复定义与一致性失控当一家 SaaS 公司推出多个子品牌或者一个平台需要支持白标White Label定制时设计系统的复杂度会呈指数级增长。典型场景包括集团旗下针对不同行业的子产品需要统一的组件行为但差异化的视觉风格、独立产品为不同客户提供可定制的 UI 主题、以及国际化产品需要适配不同地区的设计偏好。在多品牌场景下设计系统面临的核心矛盾是组件逻辑需要高度复用一个 Button 组件在所有品牌中的交互行为是一致的但视觉表现需要灵活切换颜色、圆角、字体、间距在不同品牌间差异明显。传统的做法是维护多套独立的 CSS 变量文件或主题配置但这种方式容易导致某品牌新增一个组件的视觉定义后忘记同步到其他品牌、主题变量在多套文件中重复定义导致维护成本线性增长、以及设计规范变更时需要逐一修改多套主题文件。一套系统化的多品牌架构需要解决三个核心问题Token 的单一定义源、品牌间变量映射的自动化、以及组件的品牌感知能力。二、多品牌 Token 架构的核心层次2.1 三层 Token 模型设计 Token 的三层模型是解决多品牌映射的关键抽象基础 TokenPrimitive Tokens设计语言的最原子级别定义。它们是纯粹的值不携带语义。例如blue-500: #1976D2、spacing-16: 16px、radius-md: 8px。基础 Token 由设计团队维护变更频率极低。语义 TokenSemantic Tokens将基础 Token 赋予功能性含义。例如color-primary: {blue-500}、spacing-container: {spacing-16}。同一语义 Token 在不同品牌中可以映射到不同的基础 Token。这一层是多品牌差异化的核心。组件 TokenComponent Tokens绑定到具体 UI 组件。例如button-primary-bg: {color-primary}、card-border-radius: {radius-lg}。组件 Token 通常直接引用语义 Token仅在需要组件级微调时才独立定义。三层模型的优势在于变更的影响范围可控修改基础 Token 的值会影响所有引用它的语义 Token全局变更修改语义 Token 的映射仅影响当前品牌品牌级变更修改组件 Token 仅影响特定组件。2.2 品牌映射机制的实现品牌映射的核心数据结构是一个 Token Map它将语义 Token 名称映射到不同品牌的实际值。这个映射关系需要在构建时解析并在运行时通过 CSS 变量注入到 DOM 中。两种映射策略的选择构建时静态映射在构建阶段为每个品牌生成独立的 CSS 文件。优势是运行时零开销劣势是无法在运行时动态切换品牌需要页面重新加载。运行时动态映射将所有品牌的 Token 值都编译为 CSS 变量前缀如--brand-a-color-primary运行时通过切换 CSS 类名实现品牌切换。优势是支持热切换劣势是 CSS 文件体积更大。实际项目中更常见的是混合策略默认主题使用构建时静态映射保证首屏性能品牌切换逻辑通过 CSS 变量覆盖实现如通过 JavaScript 动态修改:root上的 CSS 变量值。2.3 品牌衍生的自动化流程品牌衍生不应是每次手动复制粘贴 Token 文件。一个可持续的流程应包括品牌基类定义所有品牌共有的默认 Token 值品牌 A 作为基准。增量覆盖品牌 B 仅定义与基准有差异的 Token其余自动继承基准值。差异检测告警当基准 Token 增加新定义时自动检测哪些品牌尚未覆盖生成提醒通知。设计稿同步支持从 Figma 的 Design Token 插件导出 JSON自动转换为代码中的 Token 映射表。三、生产级多品牌系统实现以下实现展示了一个完整的多品牌设计系统包含 Token 引擎、品牌管理和组件注入/** * 多品牌设计 Token 引擎 * 实现三层 Token 模型 品牌映射 CSS 变量生成 */ // ---- Token 类型定义 ---- interface PrimitiveTokens { colors: Recordstring, string; spacing: Recordstring, string; radii: Recordstring, string; fonts: Recordstring, string; } interface SemanticTokens { colorPrimary: string; colorSecondary: string; colorBackground: string; colorSurface: string; colorText: string; colorTextSecondary: string; colorBorder: string; spacingXs: string; spacingSm: string; spacingMd: string; spacingLg: string; radiusSm: string; radiusMd: string; radiusLg: string; fontFamily: string; fontSizeBase: string; } interface ComponentTokens { button: { primaryBg: string; primaryText: string; borderRadius: string; paddingX: string; paddingY: string; }; card: { bg: string; borderRadius: string; padding: string; shadow: string; }; input: { bg: string; border: string; borderRadius: string; paddingX: string; paddingY: string; focusBorder: string; }; } // ---- 基础 Token单一定义源 ---- const primitives: PrimitiveTokens { colors: { blue-500: #1976D2, blue-600: #1565C0, green-500: #4CAF50, red-500: #F44336, orange-500: #FF9800, gray-50: #FAFAFA, gray-100: #F5F5F5, gray-200: #EEEEEE, gray-300: #E0E0E0, gray-600: #757575, gray-800: #424242, gray-900: #212121, white: #FFFFFF, black: #000000, }, spacing: { 4: 4px, 8: 8px, 12: 12px, 16: 16px, 24: 24px, 32: 32px, }, radii: { sm: 4px, md: 8px, lg: 16px, full: 9999px, }, fonts: { sans: Inter, -apple-system, BlinkMacSystemFont, sans-serif, mono: JetBrains Mono, Fira Code, monospace, }, }; // ---- 品牌映射层 ---- interface BrandConfig { id: string; name: string; // 语义 Token 从基础 Token 的映射函数 semanticResolver(primitives: PrimitiveTokens): SemanticTokens; // 组件 Token 从语义 Token 的映射函数可选覆盖 componentOverrides?: PartialComponentTokens; } // 品牌 A默认基准 const brandA: BrandConfig { id: brand-a, name: 默认品牌, semanticResolver: (p) ({ colorPrimary: p.colors[blue-500], colorSecondary: p.colors[green-500], colorBackground: p.colors[gray-50], colorSurface: p.colors[white], colorText: p.colors[gray-900], colorTextSecondary: p.colors[gray-600], colorBorder: p.colors[gray-300], spacingXs: p.spacing[4], spacingSm: p.spacing[8], spacingMd: p.spacing[16], spacingLg: p.spacing[24], radiusSm: p.radii[sm], radiusMd: p.radii[md], radiusLg: p.radii[lg], fontFamily: p.fonts[sans], fontSizeBase: 16px, }), }; // 品牌 B深色主题 橙色主调 const brandB: BrandConfig { id: brand-b, name: 企业版品牌, semanticResolver: (p) ({ colorPrimary: p.colors[orange-500], colorSecondary: p.colors[blue-500], colorBackground: p.colors[gray-900], colorSurface: p.colors[gray-800], colorText: p.colors[white], colorTextSecondary: p.colors[gray-300], colorBorder: p.colors[gray-600], spacingXs: p.spacing[4], spacingSm: p.spacing[8], spacingMd: p.spacing[16], spacingLg: p.spacing[24], radiusSm: 6px, // 品牌 B 圆角更大 radiusMd: 12px, radiusLg: 20px, fontFamily: p.fonts[sans], fontSizeBase: 16px, }), componentOverrides: { card: { // 品牌 B 的卡片有特殊阴影 shadow: 0 4px 20px rgba(0,0,0,0.3), }, }, }; // ---- Token 引擎 ---- class DesignTokenEngine { private brands: Mapstring, BrandConfig new Map(); private defaultComponentTokens!: ComponentTokens; /** * 注册品牌配置 */ registerBrand(config: BrandConfig): void { this.brands.set(config.id, config); } /** * 为指定品牌生成完整的 CSS 变量 */ generateCSSVariables(brandId: string): string { const brand this.brands.get(brandId); if (!brand) { throw new Error(品牌 ${brandId} 未注册可用品牌${[...this.brands.keys()].join(, )}); } // 1. 生成语义 Token const sematic brand.semanticResolver(primitives); // 2. 基于语义 Token 生成组件 Token const components this.buildComponentTokens(sematic, brand.componentOverrides); // 3. 拼接为 CSS 变量字符串 const lines: string[] []; lines.push(/* ${brand.name} 主题 - 自动生成 */); lines.push([data-brand brandId ] {); // 语义级变量 lines.push( --dt-color-primary: ${sematic.colorPrimary};); lines.push( --dt-color-secondary: ${sematic.colorSecondary};); lines.push( --dt-color-bg: ${sematic.colorBackground};); lines.push( --dt-color-surface: ${sematic.colorSurface};); lines.push( --dt-color-text: ${sematic.colorText};); lines.push( --dt-color-text-secondary: ${sematic.colorTextSecondary};); lines.push( --dt-color-border: ${sematic.colorBorder};); lines.push( --dt-spacing-xs: ${sematic.spacingXs};); lines.push( --dt-spacing-md: ${sematic.spacingMd};); lines.push( --dt-spacing-lg: ${sematic.spacingLg};); lines.push( --dt-radius-sm: ${sematic.radiusSm};); lines.push( --dt-radius-md: ${sematic.radiusMd};); lines.push( --dt-font-family: ${sematic.fontFamily};); // 组件级变量 lines.push( --dt-btn-primary-bg: ${components.button.primaryBg};); lines.push( --dt-btn-primary-text: ${components.button.primaryText};); lines.push( --dt-btn-radius: ${components.button.borderRadius};); lines.push( --dt-btn-px: ${components.button.paddingX};); lines.push( --dt-btn-py: ${components.button.paddingY};); lines.push( --dt-card-bg: ${components.card.bg};); lines.push( --dt-card-radius: ${components.card.borderRadius};); lines.push( --dt-card-padding: ${components.card.padding};); lines.push( --dt-card-shadow: ${components.card.shadow};); lines.push( --dt-input-bg: ${components.input.bg};); lines.push( --dt-input-border: ${components.input.border};); lines.push( --dt-input-radius: ${components.input.borderRadius};); lines.push( --dt-input-focus-border: ${components.input.focusBorder};); lines.push(}); return lines.join(\n); } /** * 生成所有已注册品牌的 CSS */ generateAllBrandsCSS(): string { const blocks: string[] []; // 首先生成默认主题不带品牌属性作为兜底 blocks.push(this.generateDefaultTheme()); // 逐个品牌生成 for (const brandId of this.brands.keys()) { blocks.push(this.generateCSSVariables(brandId)); } return blocks.join(\n\n); } /** * 生成默认降级主题无>