A complete guide on building modular, scalable, and reactive theme management in ArkTS for HarmonyOS Next applications.
Introduction
Have you ever tried switching between light and dark modes in your ArkTS app, only to find your components visually breaking or needing manual updates? In 2025, with HarmonyOS Next gaining traction, having a centralized and modular theme system isn’t just a nice-to-have — it’s a best practice.
This article walks you through building a scalable and reusable theme management system using ArkTS. We’ll show how it mirrors best practices from frameworks like Flutter, yet is tailored to HarmonyOS constraints.
In HarmonyOS Next development, especially for wearables or multi-device applications, a consistent theme is essential. Styling inconsistencies lead to poor user experience and maintenance overhead. Unlike Flutter’s Theme.of(context) or copyWith ArkTS lacks out-of-the-box theme support, which makes it important to implement a solid custom solution.
Description
We aim to:
-
Create centralized theme definitions
-
Allow runtime switching between Light and Dark modes
-
Provide reusable components that react to theme changes
-
Reduce code duplication
We’ll cover:
-
ThemeModeenum for mode management -
ColorSchemeandTextStylefor central styling -
AppThemeas the core context holder -
ThemeStorefor reactivity -
ThemedText,ThemedButton, and more
Solution / Approach
1. ThemeMode Enum
export enum ThemeMode {
LIGHT,
DARK,
}
This helps us manage modes cleanly using strict types.
2. TextStyle & ComponentStyle Interfaces
export interface TextStyle {
fontSize: number;
fontWeight: number;
color: string;
fontFamily?: string;
}
These define base styling contracts for text and UI elements.
3. ColorScheme & IColorSchemeOptions
export interface IColorSchemeOptions {
primary: string;
onPrimary: string;
background: string;
onBackground: string;
surface: string;
onSurface: string;
error: string;
onError: string;
}
export class ColorScheme {
constructor(options: IColorSchemeOptions) {
// Assign properties here
}
}
We use IColorSchemeOptions to clearly define required parameters.
4. Light & Dark Color Schemes
export const LightColorScheme = new ColorScheme({
primary: '#1976D2',
onPrimary: '#FFFFFF',
background: '#FFFFFF',
onBackground: '#000000',
surface: '#F5F5F5',
onSurface: '#000000',
error: '#D32F2F',
onError: '#FFFFFF'
});
export const DarkColorScheme = new ColorScheme({
primary: '#90CAF9',
onPrimary: '#000000',
background: '#121212',
onBackground: '#FFFFFF',
surface: '#1E1E1E',
onSurface: '#FFFFFF',
error: '#CF6679',
onError: '#000000'
});
5. AppTheme Class
export class AppTheme {
colors: ColorScheme;
textTheme: TextTheme;
componentTheme: ComponentTheme;
constructor(mode: ThemeMode) {
this.colors = mode === ThemeMode.DARK ? DarkColorScheme : LightColorScheme;
this.textTheme = new TextTheme(this.colors);
this.componentTheme = new ComponentTheme(this.colors);
}
}
AppTheme acts like the root context of all style-related definitions.
6. TextTheme and ComponentTheme
export class TextTheme {
titleLarge: TextStyle;
// ... other text styles
constructor(colors: ColorScheme) {
this.titleLarge = { fontSize: 24, fontWeight: 700, color: colors.primary };
}
}
export class ComponentTheme {
buttonStyle: ComponentStyle;
constructor(colors: ColorScheme) {
this.buttonStyle = {
backgroundColor: colors.primary,
fontColor: colors.onPrimary,
borderRadius: 12,
padding: 12
};
}
}
7. ThemeStore (Reactive Store)
@Observed
export class ThemeStore {
theme: AppTheme = new AppTheme(ThemeMode.LIGHT);
setTheme(mode: ThemeMode) {
this.theme = new AppTheme(mode);
}
}
export const themeStore = new ThemeStore();
Any UI using this store will automatically update on mode change.
8. Helper: applyTextStyle & copyWith
export function applyTextStyle(text: StylableText, style: TextStyle): StylableText {
return text
.fontSize(`${style.fontSize}fp`)
.fontWeight(style.fontWeight)
.fontColor(style.color)
.fontFamily(style.fontFamily ?? 'HarmonyOS Sans');
}
export class TextStyleImpl implements TextStyle {
// All props
copyWith(overrides: Partial): TextStyle {
return new TextStyleImpl({ ...this, ...overrides });
}
}
9. Themed Components (Reusable)
@Component
export struct ThemedText {
@Prop text: string;
@Prop style: TextStyle;
build() {
Text(this.text)
.fontSize(`${this.style.fontSize}fp`)
.fontWeight(this.style.fontWeight)
.fontColor(this.style.color)
}
}
These components consume styles from themeStore.theme.
Conclusion
Building a modular theme management system in ArkTS is not just about aesthetics — it’s about creating scalable, maintainable, and consistent experiences across your entire application.
By centralizing theme logic using AppTheme, ThemeStore, and reusable components like ThemedText developers can reduce duplication, simplify updates, and enable seamless light/dark mode switching.
If you’re building for HarmonyOS Next in 2025, this approach will save you time, prevent UI inconsistencies, and elevate your app’s user experience.
🧠 What’s next? Try extending this architecture with typography scaling, spacing tokens, or even user-selectable themes via settings.
💬 How are you handling themes in your ArkTS project? Share your tips or questions in the comments!
Key Takeaways
-
ArkTS lacks built-in theme systems, so we must build them.
-
Inspired by Flutter’s
Theme.of()andcopyWith. -
Centralization improves consistency, scalability, and DX.
-
Reactive store enables real-time theme switching.
🧠 Pro Tip: You can also structure your themes as external resource files using and access them via , but modular runtime control is still easier using in-code object-based theme stores.resources/color.json$r()
✍️Written by Arif Emre Ankara
