一张 sketch-notes 风格的框架示意图,主题是 HarmonyOS 7 颜色 Token

前言

颜色 token 的重点不是把色值藏起来,而是让颜色具备语义。品牌色、危险色、辅助文字、页面背景这些角色稳定了,主题切换才不会牵一发动全身。

这篇单独聊 品牌页 这个场景。重点不是堆 API,而是把品牌色、危险色、辅助文字和背景色按语义收口,避免主题切换时到处改组件。

为什么这个问题经常被写乱

颜色 Token 让主题更稳 这类内容很容易被写成“代码能跑就算讲完了”,但对初学者来说,这恰恰是最不够的地方。真正让人卡住的,往往不是某个组件名记不住,而是不知道这段代码为什么要这样拆、状态为什么要这样放、以后需求变化时应该从哪里改。

所以这篇文章不只想给你一个能跑的例子,更想把背后的判断过程讲清楚。你只要把这个判断过程吃透,后面自己改页面、补需求、查问题时,心里会稳很多。

场景:品牌页接入深色模式

一张 sketch-notes 风格的流程图,内容对应文章的‘实操步骤’:先按语义列出页面用色,不按

颜色 token 最容易在项目中后期暴露价值。早期页面少,按钮直接写 #0A59F7,危险操作直接写 #D92D20,看起来没什么问题。等品牌升级、深色模式上线、营销页要换主题时,散落在页面里的色值就会变成维护成本。

品牌页尤其明显:主按钮、选中态、活动标签、说明文字、分割线、卡片背景都和颜色有关。如果每个组件都直接绑定一个具体色值,主题切换时就要到处改。语义 token 的做法是让组件只关心“这是品牌色”“这是危险色”“这是弱文本”,不关心它今天具体是哪个蓝。

颜色 token 的价值不是少写几个色值,而是让主题变更不会扩散到每个组件。

常用颜色语义

一张 sketch-notes 风格的信息图,展示 HarmonyOS 7 品牌页在浅色与深色模式下

Token 浅色值 深色值 用途
colorBrand #0A59F7 #6EA8FF 主按钮、选中态
colorDanger #D92D20 #FF8A80 删除、异常
colorSuccess #0A7F3F #62D28F 成功、完成
colorTextPrimary #111111 #F2F4F8 主文案
colorTextSecondary #666666 #B8C0CC 辅助说明
colorPageBg #F5F7FA #111318 页面背景
colorCardBg #FFFFFF #1C2028 卡片背景

实操步骤

  1. 先按语义列出页面用色,不要按色相列 blue1red2
  2. 页面组件只引用 token 方法,不直接散写色值。
  3. 深浅主题切换时只改变 token 返回值,不改页面结构。
  4. 危险、成功、品牌三个语义要分开,不能拿品牌色硬顶所有状态。
  5. 检查卡片背景和页面背景的层级,暗色模式下尤其容易糊成一片。

先把页面目标想清楚

在真正写代码之前,先别急着盯着 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 调用。真实项目可以把这些方法迁到主题对象或资源文件,页面代码基本不用动。

容易踩坑的点

  • blue1blue2 命名,品牌色一换名字就失效。
  • 危险按钮复用品牌色,只靠文案表达删除风险。
  • 暗色模式只反转背景,没有调整辅助文字对比度。
  • 卡片背景和页面背景太接近,层级不清楚。
  • 同一个语义在不同页面写了不同色值,主题升级时漏改。

优化建议

颜色 token 不要一次性设计几十个。先从品牌、危险、成功、主文案、弱文案、页面背景、卡片背景开始,覆盖大多数页面后再补充分割线、遮罩、禁用态等细项。

深色模式不是简单把背景变黑。危险色、成功色、品牌色都要考虑可读性,尤其是小字号标签和弱提示文案。上线前建议在真实设备上看一遍亮度较低的场景,很多对比度问题在模拟器里不明显。

颜色 Token 要先分清语义

颜色 token 的重点不是少写几个 #,而是让每个颜色有稳定角色。品牌色、危险色、成功色、主文案、辅助文案、页面背景和卡片背景,这些语义一旦稳定,主题切换就不会扩散到每个组件。

示例里的 brand()danger()success() 分别承担不同含义。删除按钮不能因为品牌色好看就继续用品牌色,成功提示也不应该和危险提示只靠文案区分。颜色语义越清楚,用户越容易理解状态。

写在最后

暗色模式尤其要注意层级。页面背景和卡片背景如果太接近,内容会糊成一片;辅助文字如果过暗,低亮度设备上会很难读。主题适配不是简单换一组色值,而是要保证信息层级仍然成立。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐