文章目录
- 前言
- 为什么这个问题经常被写乱
- 场景:品牌页接入深色模式
- 常用颜色语义
- 实操步骤
- 先把页面目标想清楚
- 完整示例:语义颜色驱动品牌页
- 把关键代码一段段拆开
- 容易踩坑的点
- 优化建议
- 颜色 Token 要先分清语义
- 写在最后
前言
颜色 token 的重点不是把色值藏起来,而是让颜色具备语义。品牌色、危险色、辅助文字、页面背景这些角色稳定了,主题切换才不会牵一发动全身。
这篇单独聊品牌页这个场景。重点不是堆 API,而是把品牌色、危险色、辅助文字和背景色按语义收口,避免主题切换时到处改组件。
为什么这个问题经常被写乱
颜色 Token 让主题更稳 这类内容很容易被写成“代码能跑就算讲完了”,但对初学者来说,这恰恰是最不够的地方。真正让人卡住的,往往不是某个组件名记不住,而是不知道这段代码为什么要这样拆、状态为什么要这样放、以后需求变化时应该从哪里改。
所以这篇文章不只想给你一个能跑的例子,更想把背后的判断过程讲清楚。你只要把这个判断过程吃透,后面自己改页面、补需求、查问题时,心里会稳很多。
场景:品牌页接入深色模式
颜色 token 最容易在项目中后期暴露价值。早期页面少,按钮直接写#0A59F7,危险操作直接写#D92D20,看起来没什么问题。等品牌升级、深色模式上线、营销页要换主题时,散落在页面里的色值就会变成维护成本。
品牌页尤其明显:主按钮、选中态、活动标签、说明文字、分割线、卡片背景都和颜色有关。如果每个组件都直接绑定一个具体色值,主题切换时就要到处改。语义 token 的做法是让组件只关心“这是品牌色”“这是危险色”“这是弱文本”,不关心它今天具体是哪个蓝。
颜色 token 的价值不是少写几个色值,而是让主题变更不会扩散到每个组件。
常用颜色语义
| Token | 浅色值 | 深色值 | 用途 |
|---|---|---|---|
colorBrand | #0A59F7 | #6EA8FF | 主按钮、选中态 |
colorDanger | #D92D20 | #FF8A80 | 删除、异常 |
colorSuccess | #0A7F3F | #62D28F | 成功、完成 |
colorTextPrimary | #111111 | #F2F4F8 | 主文案 |
colorTextSecondary | #666666 | #B8C0CC | 辅助说明 |
colorPageBg | #F5F7FA | #111318 | 页面背景 |
colorCardBg | #FFFFFF | #1C2028 | 卡片背景 |
实操步骤
- 先按语义列出页面用色,不要按色相列
blue1、red2。 - 页面组件只引用 token 方法,不直接散写色值。
- 深浅主题切换时只改变 token 返回值,不改页面结构。
- 危险、成功、品牌三个语义要分开,不能拿品牌色硬顶所有状态。
- 检查卡片背景和页面背景的层级,暗色模式下尤其容易糊成一片。
先把页面目标想清楚
在真正写代码之前,先别急着盯着 API。更有用的做法是先想清楚:这个页面到底想解决什么问题,用户最在意的反馈是什么,哪些状态必须一直保持一致。
当你先把这条主线想明白,再回头看组件和状态设计,很多选择都会顺理成章。对小白来说,这一步尤其重要,因为它能帮你从“照着抄”慢慢过渡到“看得懂、改得动”。
完整示例:语义颜色驱动品牌页
@Entry@Componentstruct ColorTokenPage{@StatedarkMode:boolean=false@StateselectedPlan:string='pro'privatebrand():string{returnthis.darkMode?'#6EA8FF':'#0A59F7'}privatedanger():string{returnthis.darkMode?'#FF8A80':'#D92D20'}privatesuccess():string{returnthis.darkMode?'#62D28F':'#0A7F3F'}privatetextPrimary():string{returnthis.darkMode?'#F2F4F8':'#111111'}privatetextSecondary():string{returnthis.darkMode?'#B8C0CC':'#666666'}privatepageBg():string{returnthis.darkMode?'#111318':'#F5F7FA'}privatecardBg():string{returnthis.darkMode?'#1C2028':'#FFFFFF'}@BuilderPlanCard(name:string,price:string,plan:string){Column({space:8}){Row(){Text(name).fontSize(18).fontWeight(FontWeight.Bold).fontColor(this.textPrimary())Blank()Text(this.selectedPlan===plan?'已选':'可选').fontSize(12).fontColor(this.selectedPlan===plan?this.success():this.textSecondary())}.width('100%')Text(price).fontSize(24).fontWeight(FontWeight.Bold).fontColor(this.brand())Text('颜色来自语义 token,主题变化时组件不用改。').fontSize(13).fontColor(this.textSecondary())Button(this.selectedPlan===plan?'当前方案':'选择方案').enabled(this.selectedPlan!==plan).backgroundColor(this.brand()).onClick(()=>this.selectedPlan=plan)}.alignItems(HorizontalAlign.Start).padding(14).backgroundColor(this.cardBg()).borderRadius(8)}build(){Column({space:14}){Row(){Text('主题预览').fontSize(22).fontWeight(FontWeight.Bold).fontColor(this.textPrimary())Blank()Toggle({type:ToggleType.Switch,isOn:this.darkMode}).onChange((value:boolean)=>this.darkMode=value)}.width('100%')this.PlanCard('专业版','¥129/月','pro')this.PlanCard('团队版','¥399/月','team')Button('删除试用数据').width('100%').backgroundColor(this.danger())}.padding(16).height('100%').backgroundColor(this.pageBg())}}把关键代码一段段拆开
brand()、danger()、success()是语义入口。组件不需要知道浅色模式品牌色是哪个蓝,也不需要知道深色模式危险色该提亮多少,只要知道当前用途是品牌、危险还是成功。
pageBg()和cardBg()分开很重要。暗色模式下如果页面和卡片都用同一个深色,卡片层级会消失,用户很难扫读内容。
PlanCard()里没有散写业务色值,只有 token 调用。真实项目可以把这些方法迁到主题对象或资源文件,页面代码基本不用动。
容易踩坑的点
- 用
blue1、blue2命名,品牌色一换名字就失效。 - 危险按钮复用品牌色,只靠文案表达删除风险。
- 暗色模式只反转背景,没有调整辅助文字对比度。
- 卡片背景和页面背景太接近,层级不清楚。
- 同一个语义在不同页面写了不同色值,主题升级时漏改。
优化建议
颜色 token 不要一次性设计几十个。先从品牌、危险、成功、主文案、弱文案、页面背景、卡片背景开始,覆盖大多数页面后再补充分割线、遮罩、禁用态等细项。
深色模式不是简单把背景变黑。危险色、成功色、品牌色都要考虑可读性,尤其是小字号标签和弱提示文案。上线前建议在真实设备上看一遍亮度较低的场景,很多对比度问题在模拟器里不明显。
颜色 Token 要先分清语义
颜色 token 的重点不是少写几个#,而是让每个颜色有稳定角色。品牌色、危险色、成功色、主文案、辅助文案、页面背景和卡片背景,这些语义一旦稳定,主题切换就不会扩散到每个组件。
示例里的brand()、danger()、success()分别承担不同含义。删除按钮不能因为品牌色好看就继续用品牌色,成功提示也不应该和危险提示只靠文案区分。颜色语义越清楚,用户越容易理解状态。
写在最后
暗色模式尤其要注意层级。页面背景和卡片背景如果太接近,内容会糊成一片;辅助文字如果过暗,低亮度设备上会很难读。主题适配不是简单换一组色值,而是要保证信息层级仍然成立。