HarmonyOS7 颜色 Token 让主题更稳:ArkUI/ArkTS 实战拆解

文章目录
前言
颜色 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
@Component
struct ColorTokenPage {
@State darkMode: boolean = false
@State selectedPlan: string = 'pro'
private brand(): string { return this.darkMode ? '#6EA8FF' : '#0A59F7' }
private danger(): string { return this.darkMode ? '#FF8A80' : '#D92D20' }
private success(): string { return this.darkMode ? '#62D28F' : '#0A7F3F' }
private textPrimary(): string { return this.darkMode ? '#F2F4F8' : '#111111' }
private textSecondary(): string { return this.darkMode ? '#B8C0CC' : '#666666' }
private pageBg(): string { return this.darkMode ? '#111318' : '#F5F7FA' }
private cardBg(): string { return this.darkMode ? '#1C2028' : '#FFFFFF' }
@Builder
PlanCard(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() 分别承担不同含义。删除按钮不能因为品牌色好看就继续用品牌色,成功提示也不应该和危险提示只靠文案区分。颜色语义越清楚,用户越容易理解状态。
写在最后
暗色模式尤其要注意层级。页面背景和卡片背景如果太接近,内容会糊成一片;辅助文字如果过暗,低亮度设备上会很难读。主题适配不是简单换一组色值,而是要保证信息层级仍然成立。
更多推荐


所有评论(0)