行业资讯
设计Token运行时动态切换:让多主题系统真正活起来的前端工程化方案
设计Token运行时动态切换让多主题系统真正活起来的前端工程化方案设计系统做好了Token 定义好了暗色模式也支持了——然后呢大多数团队做到这里就停了。但真正的多主题系统不应该只是亮/暗两个选项而应该能在运行时动态切换、按需加载、甚至让用户自定义。这篇文章我要讲的就是如何让设计 Token 真正活起来。一、从静态Token到动态系统多主题架构的演进路线大多数团队的设计 Token 流程是这样的设计稿 → Style Dictionary → 生成 CSS 变量 → 写死在代码里。这种方式在只有亮/暗两个主题时勉强够用但一旦主题数量增加品牌定制、用户自选、节日主题……就会暴露出架构上的问题。三种主题架构模式对比结论模式二CSS变量 data-theme是性价比最高的方案也是本文的重点。设计Token的三层结构要让主题系统真正灵活需要把 Token 分成三层设计Token三层结构 ├── 第一层原始值Source of Truth │ └── tokens/ 目录下的 JSON 文件品牌色/间距/字体... │ ├── 第二层主题变体Theme Variants │ └── 每个主题对应的 Token 映射light/dark/brand-a/brand-b... │ └── 第三层运行时绑定Runtime Binding └── CSS自定义属性 或 JS对象在运行时被切换用Style Dictionary构建Token管道Style Dictionary 是 Amazon 开源的设计 Token 构建工具也是目前最成熟的方案。安装与配置npm install style-dictionary --save-dev目录结构推荐design-tokens/ ├── tokens/ │ ├── color/ │ │ ├── core.json # 核心色品牌色/辅助色 │ │ └── semantic.json # 语义色text/background/border │ ├── spacing/ │ │ └── core.json # 间距Token │ ├── typography/ │ │ └── core.json # 字体Token │ └── themes/ │ ├── light.json # 亮色主题覆盖 │ └── dark.json # 暗色主题覆盖 ├── config.json # Style Dictionary 配置 └── build/ └── 生成的CSS/SCSS/JS文件tokens/color/core.json示例直接可用{ color: { core: { blue: { 500: { value: #0066FF, type: color, comment: 品牌主色 } }, gray: { 100: { value: #F5F5F7, type: color }, 200: { value: #E8E8ED, type: color }, 500: { value: #80808A, type: color }, 800: { value: #15151A, type: color } } }, semantic: { background: { primary: { value: {color.core.blue.500}, type: color, comment: 主要背景色 } }, text: { primary: { value: {color.core.gray.800}, type: color, comment: 主要文字颜色 } } } } }config.json配置关键{ source: [tokens/**/*.json], platforms: { css: { transformGroup: css, prefix: token, buildPath: build/css/, files: [ { destination: _variables.css, format: css/variables } ] }, scss: { transformGroup: scss, buildPath: build/scss/, files: [ { destination: _variables.scss, format: scss/variables } ] } } }运行npx style-dictionary build就会生成/* build/css/_variables.css */ :root { --token-color-core-blue-500: #0066FF; --token-color-core-gray-100: #F5F5F7; --token-color-semantic-background-primary: var(--token-color-core-blue-500); --token-color-semantic-text-primary: var(--token-color-core-gray-800); }二、运行时动态切换CSS变量的主题切换完整方案Token 生成好了下一步是让主题能在运行时切换。核心思路是用[data-theme]属性选择器配合 CSS 自定义属性实现无刷新的主题切换。基础方案data-theme CSS变量第一步定义主题变量/* theme.css */ /* 亮色主题默认 */ :root, [data-themelight] { --color-bg-primary: #FFFFFF; --color-bg-secondary: #F5F5F7; --color-text-primary: #1A1A2E; --color-text-secondary: #80808A; --color-brand-primary: #0066FF; --radius-default: 12px; --spacing-unit: 16px; } /* 暗色主题 */ [data-themedark] { --color-bg-primary: #15151A; --color-bg-secondary: #2C2C31; --color-text-primary: #F5F5F7; --color-text-secondary: #AAAAAF; --color-brand-primary: #4D9FFF; /* 暗色下用更亮的蓝色保证对比度 */ --radius-default: 12px; /* 圆角在暗色下通常保持不变 */ --spacing-unit: 16px; } /* 品牌主题A如企业客户定制 */ [data-themebrand-a] { --color-bg-primary: #FFFFFF; --color-brand-primary: #E94560; /* 品牌A的主色红色系 */ /* 其他 Token 可以继承 light 主题的值 */ } /* 高对比度主题无障碍 */ [data-themehigh-contrast] { --color-text-primary: #000000; --color-text-secondary: #333333; --color-bg-primary: #FFFFFF; --color-brand-primary: #0047AB; /* 更深的蓝色提高对比度 */ }第二步在CSS中使用Token/* 用 Token 写样式而不是直接用色值 */ .card { background: var(--color-bg-primary); color: var(--color-text-primary); border-radius: var(--radius-default); padding: var(--spacing-unit); border: 1px solid var(--color-brand-primary); }第三步用JS切换主题// theme-switcher.js // 获取当前主题 export function getCurrentTheme() { return document.documentElement.getAttribute(data-theme) || light; } // 切换主题 export function setTheme(themeName) { // 设置>// auto-dark.js // 根据亮色 Token自动计算暗色值 function generateDarkTokens(lightTokens) { const darkTokens {}; for (const [key, value] of Object.entries(lightTokens)) { if (key.startsWith(color-bg)) { // 背景色取对应的暗色版本 darkTokens[key] invertLightness(value); } else if (key.startsWith(color-text)) { // 文字颜色取背景色的反色 darkTokens[key] lightTokens[key.replace(text, bg)]; } else if (key.startsWith(color-brand)) { // 品牌色增加亮度保证在暗色背景上的对比度 darkTokens[key] lighten(value, 20); // 亮度20% } else { // 其他 Token保持不变 darkTokens[key] value; } } return darkTokens; } // 辅助函数增加颜色亮度 function lighten(hex, percent) { const num parseInt(hex.slice(1), 16); const r Math.min(255, (num 16) Math.round(255 * percent / 100)); const g Math.min(255, ((num 8) 0x00FF) Math.round(255 * percent / 100)); const b Math.min(255, (num 0x0000FF) Math.round(255 * percent / 100)); return #${(0x1000000 r * 0x10000 g * 0x100 b).toString(16).slice(1)}; }更成熟的方案使用 Material Design 的 HCT 色彩空间 来自动生成暗色主题——它能保证亮暗主题之间的感知一致性。三、设计Token的工程化从定义到交付的完整流程设计 Token 不只是变量定义更是一套连接设计工具、代码仓库、和最终产品的工程化管道。以下是我实践的完整流程。完整流程图Token Studio插件设计工具与代码的桥梁Token Studio原名 Figma Tokens是目前最好的设计 Token 管理插件它可以直接从 Figma 导出 Token 定义JSON格式然后交给 Style Dictionary 构建。工作流程设计师在 Figma 里用 Token Studio 插件定义 Design Token插件自动同步到 JSON 文件通过 GitHub SyncCI/CD 检测到 Token 文件变化自动运行 Style Dictionary 构建构建产物CSS/SCSS/JS自动发布到 npm 或 CDN前端应用引用最新的 Token 文件Token Studio 的 JSON 格式示例{ colors: { brand: { primary: { $value: #0066FF, $type: color, $description: 品牌主色 } } }, spacing: { sm: { $value: 8px, $type: dimension }, md: { $value: 16px, $type: dimension }, lg: { $value: 24px, $type: dimension } } }与Flutter对接跨端Token管道如果团队同时维护 Web 和 Flutter 应用需要一套 Token 定义多端生成。Style Dictionary 支持自定义格式可以生成 Flutter 的ThemeData// style-dictionary.config.js // 自定义格式生成 Flutter Dart 代码 module.exports { source: [tokens/**/*.json], platforms: { // ... 其他平台 flutter: { transformGroup: flutter, buildPath: build/flutter/, files: [ { destination: app_tokens.dart, format: flutter/class.dart, // 需要自定义format filter: { type: color, }, }, ], }, }, };生成的 Dart 代码// app_tokens.dart // 由 Style Dictionary 自动生成不要手动修改 import package:flutter/material.dart; abstract class AppTokens { // 颜色 Token static const brandPrimary Color(0xFF0066FF); static const brandPrimaryDark Color(0xFF4D9FFF); // 间距 Token static const spacingSm 8.0; static const spacingMd 16.0; static const spacingLg 24.0; // 主题数据 static ThemeData lightTheme ThemeData( primaryColor: brandPrimary, colorScheme: ColorScheme.light( primary: brandPrimary, background: const Color(0xFFFFFFFF), ), ); static ThemeData darkTheme ThemeData( primaryColor: brandPrimaryDark, colorScheme: ColorScheme.dark( primary: brandPrimaryDark, background: const Color(0xFF15151A), ), ); }四、高级话题动态主题与用户自定义基础的多主题切换做完了来聊两个高级话题动态加载主题和用户自定义主题。这两个功能是让设计系统真正活起来的关键。动态加载主题按需加载CSS如果主题数量很多如 SaaS 产品的企业定制主题不应该在一次加载中把所有主题的 CSS 都下载下来。正确的做法是按需加载。// dynamic-theme-loader.js const loadedThemes new Set(); export async function loadTheme(themeName) { // 已加载过的主题直接切换 if (loadedThemes.has(themeName)) { setTheme(themeName); return; } // 动态加载主题 CSS const link document.createElement(link); link.rel stylesheet; link.href /themes/${themeName}.css; await new Promise((resolve, reject) { link.onload () { loadedThemes.add(themeName); resolve(); }; link.onerror reject; }); // 加载完成后切换 setTheme(themeName); }主题 CSS 文件的格式/* themes/brand-a.css */ /* 只需要覆盖需要变化的 Token不需要重新定义所有 Token */ [data-themebrand-a] { --color-brand-primary: #E94560; /* 品牌A的主色 */ --color-brand-secondary: #FF6B35; /* 其他 Token 自动继承 :root 的定义 */ }用户自定义主题让用户输入色值更高级的功能是让用户自己选颜色实时预览主题效果。这需要把 Token 的动态替换逻辑放到 JS 里而不是纯 CSS。// user-theme.js export function applyUserTheme(colors) { const root document.documentElement; // 用户选择的颜色 root.style.setProperty(--color-brand-primary, colors.primary); root.style.setProperty(--color-brand-secondary, colors.secondary); // 自动计算衍生色如hover态的深色版本 root.style.setProperty( --color-brand-primary-hover, darken(colors.primary, 10) // 加深10% ); // 标记为用户自定义主题 root.setAttribute(data-theme, custom); } // 颜色操作工具函数 function darken(hex, percent) { // ... 实现加深逻辑 } function lighten(hex, percent) { // ... 实现变浅逻辑 }实时预览的实现用户选颜色时需要实时预览效果。这要求 Token 的替换是即时的。// 颜色选择器 实时预览 document.getElementById(primary-color-picker).addEventListener(input, (e) { const color e.target.value; // 即时替换 Token无需重新加载页面 document.documentElement.style.setProperty(--color-brand-primary, color); // 同时更新预览区域的样式 document.querySelectorAll(.preview-element).forEach(el { el.style.backgroundColor color; }); });性能优化如果页面中有大量使用 Token 的元素频繁替换 CSS 变量可能导致性能问题。解决方案// 用 requestAnimationFrame 节流 let pendingUpdate false; function scheduleThemeUpdate() { if (!pendingUpdate) { pendingUpdate true; requestAnimationFrame(() { applyPendingThemeUpdates(); pendingUpdate false; }); } }五、总结设计 Token 的运行时动态切换不是多了个换肤功能这么简单。它背后是一套连接设计语义、代码实现、和用户偏好的完整系统。做好了设计系统就能真正活起来——不再是一套静态的规范文档而是一个能适应不同场景、不同用户、不同品牌的动态系统。从美院到前端工程化我越来越觉得设计系统就像乐谱Token 是音符而主题切换就是不同的演奏版本。同一首曲子可以是钢琴独奏亮色主题也可以是弦乐四重奏暗色主题——但音符Token是同一套只是编排方式不同。关键要点三层结构原始值 → 主题变体 → 运行时绑定职责分离CSS变量 >
郑州网站建设
网页设计
企业官网