
深色模式不是把页面背景改黑这么简单。真正落地时,应用会同时遇到资源目录、状态栏、WebView、前端页面、网络图片、特殊页面覆盖层等多条链路。如果这些链路各自处理,后期很容易出现切换不一致、局部闪烁、图片过暗或业务页面遗漏的问题。
这篇文章整理一套 HarmonyOS 深色模式适配思路:先把系统/应用模式统一收口,再让资源、WebView 和图片渲染分别接入。文中的代码均为脱敏示例,类名、资源名和页面名都已替换为通用命名。
目录
- 先明确适配边界
- 推荐落地结构
- 用
dark资源集承接本地资源 - 应用级模式切换要集中管理
- 状态栏要单独适配
- WebView 优先让网页自己支持深色模式
- Web 页面不支持时,用覆盖层做兜底
- 网络图片用
AttributeModifier做低侵入统一处理 - 验收清单
先明确适配边界
一次完整的深色模式适配至少要覆盖五类对象:
- 本地颜色和图片资源:优先交给资源目录自动切换。
- 应用级模式:支持跟随系统、浅色、深色三种状态。
- 系统栏:状态栏文字颜色要跟随当前背景反转。
- WebView:如果网页本身支持深色模式,优先让 Web 按 CSS 媒体查询切换。
- 网络图片:不能靠资源目录,需要运行时统一调节亮度、饱和度或做定制替换。

可以把适配对象按"是否可由系统自动切换"先分层:
| 适配对象 | 推荐方案 | 适用场景 | 风险点 |
|---|---|---|---|
| 本地颜色、本地图片 | base / dark 资源集 |
颜色、占位图、切片 | 资源名不统一会导致兜底失败 |
| 应用模式 | ApplicationContext.setColorMode() |
跟随系统、手动浅色、手动深色 | 入口分散会导致页面状态不一致 |
| 状态栏 | 统一窗口工具方法 | 顶部深浅背景切换 | 页面单独设置容易互相覆盖 |
| 自有 Web 页面 | Web.darkMode() + CSS 媒体查询 |
H5 可改造 | 前端没写深色 CSS 时不会自动变好 |
| 不可改造 Web 页面 | 半透明覆盖层兜底 | 历史页、三方页 | 可能二次变暗,只适合过渡 |
| 网络图片 | AttributeModifier<ImageAttribute> |
通用网络图降亮度、降饱和 | 透明图、Logo、活动图要例外处理 |
推荐落地结构
最终结构建议按下面的职责拆分:
EntryAbility:监听系统配置变化,初始化应用模式。ThemeManager:记录当前模式,统一通知各层更新。- 资源目录:承接本地颜色和本地图片自动切换。
- 系统栏工具:集中设置状态栏颜色。
- Web 容器:优先使用
darkMode(WebDarkMode.Auto),必要时提供覆盖层兜底。 - 图片 Modifier:对网络图做亮度、饱和度调整。
- 特殊页面:单独做图片、背景、透明度等定制处理。
这种拆法的核心是:模式判断只做一次,后续各层只关心"现在是不是深色模式"。这样后续新增页面、替换 WebView 或调整图片策略时,改动面会小很多。
1. 用 dark 资源集承接本地资源
本地颜色、切片、占位图这类资源,优先通过 resources 下的 base 和 dark 资源集做自动适配。创建 dark 资源集时,需要选择 Color Mode = Dark,这样同名资源才能在深色模式下被系统自动命中。
推荐把语义稳定的资源名保留下来,只在不同资源集中提供不同值:
text
src/main/resources/
base/element/color.json
dark/element/color.json
base/media/placeholder.png
dark/media/placeholder.png
例如浅色和深色都使用同一个资源名:
typescript
Column()
.backgroundColor($r('app.color.page_bg'))
Image($r('app.media.placeholder'))
这样业务代码不需要判断当前模式,系统会根据当前颜色模式解析到对应资源。
2. 应用级模式切换要集中管理
应用一般需要提供三种模式:
- 跟随系统:
ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET - 深色模式:
ConfigurationConstant.ColorMode.COLOR_MODE_DARK - 浅色模式:
ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT
注意:以当前 HarmonyOS 官方文档为准,COLOR_MODE_NOT_SET 表示不设置颜色模式,也就是跟随系统;setColorMode 从 API 11 开始可用,且官方说明仅支持主线程调用。
typescript
import { common, ConfigurationConstant } from '@kit.AbilityKit';
export enum ThemeMode {
System = 'system',
Light = 'light',
Dark = 'dark',
}
export function toColorMode(mode: ThemeMode): ConfigurationConstant.ColorMode {
switch (mode) {
case ThemeMode.Dark:
return ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
case ThemeMode.Light:
return ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
default:
return ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET;
}
}
export function applyAppColorMode(context: common.Context, mode: ThemeMode): void {
const applicationContext = context.getApplicationContext();
applicationContext.setColorMode(toColorMode(mode));
}
更推荐把模式切换收口到一个管理类里,由它负责记录当前模式、派发通知,并驱动特殊页面、Web 层和图片层更新。
typescript
export class ThemeManager {
private static currentMode: ThemeMode = ThemeMode.System;
private static listeners: Array<(isDark: boolean) => void> = [];
static update(context: common.Context, mode: ThemeMode): void {
this.currentMode = mode;
applyAppColorMode(context, mode);
this.notify(this.isDarkMode());
}
static onChanged(listener: (isDark: boolean) => void): void {
this.listeners.push(listener);
}
static isDarkMode(): boolean {
return this.currentMode === ThemeMode.Dark;
}
static notifySystemChanged(isDark: boolean): void {
if (this.currentMode === ThemeMode.System) {
this.notify(isDark);
}
}
private static notify(isDark: boolean): void {
this.listeners.forEach((listener) => listener(isDark));
}
}
如果应用需要响应系统深浅色变化,可以在 Ability 中监听配置更新,并把结果同步给统一的管理类:
typescript
import { Configuration, ConfigurationConstant } from '@kit.AbilityKit';
onConfigurationUpdate(newConfig: Configuration): void {
const isDark =
newConfig.colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
ThemeManager.notifySystemChanged(isDark);
updateSystemBarStyle(isDark);
}
3. 状态栏要单独适配
很多深色模式问题不是内容区造成的,而是状态栏文字颜色没有切换。状态栏通常需要跟着页面背景反转:浅色背景用深色文字,深色背景用浅色文字。
typescript
import { window } from '@kit.ArkUI';
export function updateSystemBarStyle(isDark: boolean, win?: window.Window): void {
win?.setWindowSystemBarProperties({
statusBarContentColor: isDark ? '#E5FFFFFF' : '#000000',
});
}
这里建议只在窗口级工具方法里设置系统栏,不要散落在每个页面里,否则后续切换模式、打开弹窗、进入沉浸页时会互相覆盖。
4. WebView:优先让网页自己支持深色模式
如果 Web 页面本身支持深色模式,HarmonyOS 的 Web 组件可以通过 darkMode() 配置 Web 深色模式。官方文档说明,WebDarkMode.Auto 表示开启深色模式并跟随系统,页面需要通过 prefers-color-scheme 定义深色样式。 
ArkTS 侧示例:
typescript
import { webview } from '@kit.ArkWeb';
@Entry
@Component
struct WebContainer {
private controller: webview.WebviewController = new webview.WebviewController();
@State private mode: WebDarkMode = WebDarkMode.Auto;
build() {
Column() {
Web({
src: $rawfile('index.html'),
controller: this.controller,
})
.darkMode(this.mode)
}
}
}
前端页面侧示例:

html
<style>
@media (prefers-color-scheme: dark) {
.page {
background: #000000;
color: #ffffff;
}
.link {
color: #317af7;
}
}
</style>
这里有一个关键判断:如果前端页面已经有深色样式,应用侧只负责把模式传递给 WebView;如果前端页面没有深色样式,不建议默认强制反色,因为强制转换无法保证所有颜色都符合预期。官方也建议强制深色模式要配合 forceDarkAccess() 使用,并注意转换结果不可完全控。
5. Web 页面不支持时,用覆盖层做兜底
对于历史 Web 页面、三方页面或暂时无法改前端代码的页面,可以在 WebView 上层盖一个半透明深色层作为兜底。这种方式侵入小,但它不是完整深色适配,只适合过渡。

typescript
Stack() {
Web({
src: this.webUrl,
controller: this.controller,
})
Column()
.width('100%')
.height('100%')
.backgroundColor(Color.Black)
.opacity(0.3)
.enabled(false)
.visibility(this.showDarkOverlay ? Visibility.Visible : Visibility.Hidden)
}
覆盖层方案的优点是接入快,缺点也很明显:
- 页面图片、文字、背景会一起变暗,不是真正的语义配色。
- 对已经支持深色的页面可能造成二次变暗。
- 遮罩透明度需要按页面类型调试,很难全局统一。
因此更合理的策略是:自有 Web 页面优先改 CSS,兜底层只服务无法改造的页面。
6. 网络图片用 AttributeModifier 做低侵入统一处理
本地资源可以通过 dark 资源集切换,但网络图不行。网络图的目标通常不是"变黑",而是在深色背景下降低刺眼程度,并尽量保持内容可识别。
可以从两个属性开始:
brightness(value):调节亮度。saturate(value):调节饱和度。
官方 AttributeModifier<T> 支持动态设置组件属性,开发者可以自定义 class 实现 AttributeModifier<ImageAttribute>,再通过 attributeModifier() 绑定到组件上。需要注意,官方文档也说明它不支持自定义组件,且不要和普通链式属性重复设置同一属性,避免刷新时生效顺序不符合预期。
typescript
export class ImageThemeModifier implements AttributeModifier<ImageAttribute> {
private isDark: boolean = false;
setDarkMode(isDark: boolean): void {
this.isDark = isDark;
}
applyNormalAttribute(instance: ImageAttribute): void {
if (this.isDark) {
instance.brightness(0.82);
instance.saturate(0.72);
} else {
instance.brightness(1);
instance.saturate(1);
}
}
}
使用时可以把 Modifier 实例放在图片封装层或页面状态里:
typescript
@State private imageModifier: ImageThemeModifier = new ImageThemeModifier();
aboutToAppear(): void {
ThemeManager.onChanged((isDark) => {
this.imageModifier.setDarkMode(isDark);
});
}
build() {
Image(this.imageUrl)
.width('100%')
.objectFit(ImageFit.Cover)
.attributeModifier(this.imageModifier)
}
这个方案适合统一降低网络图亮度和饱和度,但并不适合所有图片。异形透明图、带阴影的 PNG、品牌 Logo、运营活动图都要谨慎处理,否则可能出现组件范围内阴影、透明区域发灰或品牌色失真。对这类图片,更稳妥的方式是加白名单、黑名单或提供深色版图片。
验收清单
深色模式的验收不能只看单个页面截图,建议至少覆盖下面这些场景:
| 场景 | 验收点 |
|---|---|
| 跟随系统 | 系统从浅色切到深色后,应用资源、状态栏、WebView、网络图都同步变化 |
| 应用内手动切换 | 选择浅色或深色后,不再被系统当前模式误覆盖 |
| 冷启动恢复 | 杀进程后重新进入,模式状态和资源解析一致 |
| 页面返回栈 | 从深色页返回浅色背景页,状态栏颜色恢复正常 |
| WebView | 自有 H5 走 prefers-color-scheme,不支持的页面才显示兜底层 |
| 网络图片 | 普通网络图不刺眼,Logo、透明图、活动图不失真 |
| 特殊页面 | 弹窗、协议页、沉浸页、独立安全模式页面等独立背景页面逐一检查 |
如果只能优先做一部分,建议先做"资源集 + 应用模式入口 + 状态栏",这三项会决定大多数页面的基础体验;WebView 和网络图片可以在统一入口稳定后逐步接入。
总结
HarmonyOS 深色模式适配可以按优先级推进:
- 本地资源先用
base/dark资源集自动切换。 - 应用级模式用
setColorMode()统一设置,并集中管理状态。 - 状态栏、WebView、网络图分别接入统一的模式通知。
- Web 页面优先支持
prefers-color-scheme,覆盖层只做兜底。 - 网络图用
AttributeModifier<ImageAttribute>做低侵入统一处理,但要为特殊图片预留例外机制。
适配完成后,建议至少覆盖三类测试:跟随系统切换、应用内手动切换、冷启动后模式恢复。深色模式的问题往往不是单点 API,而是链路一致性;只要把模式入口收口,后续维护会轻很多。