Frontend Development 18 min read

ArkTS Component Development Basics: Project Creation, Decorators, Styles, Events, State Management, and Slots

This tutorial walks through the fundamentals of building ArkTS components for HarmonyOS, covering project setup, key decorators like @Entry and @Component, styling with @Styles and @Extend, handling events, managing properties and state, conditional rendering, functional components, and slot implementation.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
ArkTS Component Development Basics: Project Creation, Decorators, Styles, Events, State Management, and Slots

Introduction

I had the opportunity to participate in the HarmonyOS conversion of our factory app, learning ArkTS and the related IDE, and exchanging ideas with HarmonyOS experts on the issue tracker.

This article, from a web‑frontend perspective, briefly discusses basic ArkTS component development topics such as property passing, slots, and conditional rendering.

Creating a Project

The project creation process is straightforward and therefore omitted.

Component and Page

After creating the project, the IDE automatically opens the initial page. The generated code looks like this:

@Entry
@Component
struct Index {
    @State message: string = 'Hello World';
    build() {
        RelativeContainer() {
            Text(this.message)
                .id('HelloWorld')
                .fontSize(50)
                .fontWeight(FontWeight.Bold)
                .alignRules({
                    center: { anchor: '__container__', align: VerticalAlign.Center },
                    middle: { anchor: '__container__', align: HorizontalAlign.Center }
                })
        }
        .height('100%')
        .width('100%')
    }
}

The @Entry decorator marks the page as an independent Page that can be navigated to via the router. @Component wraps the object for rendering and creates a MVVM‑like data‑to‑view update flow, similar to React.Component or Vue.defineComponent . The build method corresponds to render() in React or setup() in Vue; it must be declared when @Component is used.

@Entry indicates a standalone page.

@Component enables data‑to‑view binding.

build performs the rendering logic.

Inside build() only declarative expressions are allowed, effectively a JSX‑style variant.

// Example of a simple return expression
export default () => (
Hello World
)
@Component
export default struct SomeComponent {
    build() {
        // console.log(123) // not allowed
        Text('Hello World')
    }
}

Independent Component

The previous example does not use @Entry ; it is a complete component declaration on its own.

Extracting the component into its own file:

@Component
export struct CustomButton {
    build() {
        Button('My Button')
    }
}

Using the component with a flex layout:

import { CustomButton } from './CustomButton'

@Entry
@Component
struct Index {
    @State message: string = 'Hello World';
    build() {
        Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
            Text(this.message)
                .id('HelloWorld')
                .fontSize(50)
                .fontWeight(FontWeight.Bold)
            CustomButton()
        }
        .height('100%')
        .width('100%')
    }
}

Style Clusters

Unlike web front‑end CSS, ArkTS does not have a separate CSS file; instead, styles are expressed via chained method calls. The @Styles decorator defines reusable style clusters, while @Extend can extend existing components.

@Styles Decorator

@Entry
@Component
struct Index {
    @State message: string = 'Hello World';
    // Declare a style cluster
    @Styles
    HelloWorldStyle() {
        .backgroundColor(Color.Yellow)
        .border({ width: { bottom: 5 }, color: '#ccc' })
        .margin({ bottom: 10 })
    }
    build() {
        Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
            Text(this.message)
                .id('HelloWorld')
                .fontSize(50)
                .fontWeight(FontWeight.Bold)
                .HelloWorldStyle()
            CustomButton()
        }
        .height('100%')
        .width('100%')
    }
}

@Styles can also decorate a plain function to create a style helper.

@Styles
function HelloWorldStyle2() {
    .backgroundColor(Color.Yellow)
    .border({ width: { bottom: 5 }, color: '#000' })
    .margin({ bottom: 10 })
}

For properties that are only available on certain components (e.g., fontSize , fontColor ), @Extend is required.

@Extend Decorator

@Extend(Text)
function TextStyle() {
    .fontSize(50)
    .fontWeight(FontWeight.Bold)
    .id('HelloWorld')
}

@Extend can also accept parameters:

@Extend(Text)
function TextStyle(fontSize: number = 50, fontColor: ResourceStr | Color = '#f00') {
    .fontSize(fontSize)
    .fontColor(fontColor)
    .fontWeight(FontWeight.Bold)
    .id('HelloWorld')
}

Event Callbacks

Events can be attached using the .onClick method. Example using promptAction.showToast to display a toast:

import { promptAction } from '@kit.ArkUI'

@Component
export struct CustomButton {
    build() {
        Column() {
            Button('My Button')
                .onClick(() => {
                    promptAction.showToast({ message: 'You clicked me!' })
                })
        }
    }
}

Note that ArkTS does not support event bubbling or capture; events are handled directly on the target component.

Properties and State

In the MV architecture, data models are divided into property (read‑only values passed from parent) and state (private mutable values). ArkTS provides @Prop for properties and @State for internal state, similar to React's props and useState .

@State Decorator

The message field in earlier examples is declared with @State , making it reactive.

@Prop Decorator

Parent components can pass values to child components via @Prop . Example adding a text property to CustomButton :

@Component
export struct CustomButton {
    @Prop text: string = 'My Button'
    build() {
        Column() {
            Button(this.text)
                .onClick(() => { /* ... */ })
        }
    }
}

Usage from the parent:

CustomButton({ text: 'My Button' })

Changing Properties and State

By exposing a count property and a custom onClickMyButton event, the parent can synchronize a click counter with the child component.

@Entry
@Component
struct Index {
    @State message: string = 'Hello World';
    @State clickCount: number = 0;
    @Styles
    HelloWorldStyle() { /* ... */ }
    build() {
        Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
            Text(this.message)
                .TextStyle(36, '#06c')
                .HelloWorldStyle2()
            CustomButton({
                text: 'Click Count',
                count: this.clickCount,
                onClickMyButton: () => { this.clickCount += 1 }
            })
        }
        .height('100%')
        .width('100%')
    }
}

@Watch Decorator

@Watch can monitor changes to a @Prop and invoke a handler:

@Prop @Watch('onChange') count: number = 0
private onChange(propName: string) {
    console.log('>>>', propName)
}

When the property changes, onChange receives the property name, allowing a single method to handle multiple watched properties.

Conditional Rendering

ArkTS supports classic if…else statements inside build() , avoiding the limitations of JSX ternary expressions.

build() {
    Column() {
        Button(`${this.text}(${this.count} x 2 = ${this.double})`).onClick(() => { /* ... */ })
        if (this.count % 2 === 0) {
            Text('Even').fontColor(Color.Red).margin({ top: 10 })
        } else {
            Text('Odd').fontColor(Color.Blue).margin({ top: 10 })
        }
    }
    .onAttach(() => { this.onChange() })
}

Functional Components

Functions annotated with @Builder become reusable UI fragments. Arrow functions are **not** supported; only regular function declarations can be used.

@Builder
function ItalicText(content: string) {
    Text(content).fontSize(14).fontStyle(FontStyle.Italic).margin({ bottom: 10 })
}

The builder can be invoked inside build() just like a component.

Implementing Slots

ArkTS provides the @BuilderParam decorator to allow a component to accept a slot function. The slot is invoked inside the component’s layout.

@Component
export struct CustomButton {
    @Prop text: string = 'My Button'
    @Prop @Watch('onChange') count: number = 0
    @State private double: number = 0
    @BuilderParam slot: () => void
    private onChange() { this.double = this.count * 2 }
    build() {
        Column() {
            Button(`${this.text}(${this.count} x 2 = ${this.double})`).onClick(() => { if (typeof this.onClickMyButton === 'function') this.onClickMyButton() })
            if (typeof this.slot === 'function') this.slot()
        }
        .onAttach(() => { this.onChange() })
    }
}

Parent usage with a slot:

CustomButton({
    text: 'Click Count',
    count: this.clickCount,
    onClickMyButton: () => { this.clickCount += 1 },
    slot: () => { this.SubTitle() }
})

Only one parameter‑less @BuilderParam is allowed per component; multiple slots require distinct names and must be invoked separately.

Conclusion

For developers already familiar with TypeScript, ArkTS feels approachable after a brief overview of its syntax, decorators, and standard library. The main pain points are limited type inference, incomplete documentation, and the lack of hot‑reload in the simulator.

FrontendUIHarmonyOSArkTSComponent Development
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.