Creating a Custom Component
In ArkUI, components refer to the elements displayed on the UI. They fall into two categories: built-in components (provided by the ArkUI framework out of the box) and custom components (defined by developers). While it is technically possible to build an entire UI using only built-in components, this approach often results in a monolithic structure, leading to low code maintainability and suboptimal performance. A well-designed UI requires careful planning, balancing factors such as code reusability, the separation of service logic from UI layers, and adaptability to version evolution. Creating custom components, which encapsulate UI elements and service logic, serves as a critical step in achieving this goal.
Custom components offer the following features:
-
Combinability: You can combine built-in components and other components, as well as their attributes and methods.
-
Reusability: Custom components can be reused across different components, serving as distinct instances in various parent components or containers.
-
Data-driven update: Custom components can hold internal state variables. When these state variables change, UI re-rendering is triggered.
NOTE
Starting from API version 24, you can enable custom components to support cross-Ability migration by configuring the metadata in the module.json5 configuration file of your application project. The configuration method is as follows: Add name as enableCustomComponentCrossAbility and value as true. Since custom components provide UIAbility, the term ability here specifically refers to UIAbility. For details, see Cross-Ability Migration of Custom Components.
Basic Usage of Custom Components
The following example shows the basic usage of a custom component.
@Component
struct HelloComponent {
@State message: string = 'Hello, World!';
build() {
// The HelloComponent custom component combines the Row and Text built-in components.
Row() {
Text(this.message)
.onClick(() => {
// The change of the state variable message drives the UI to be re-rendered. As a result, the text changes from "Hello, World!" to "Hello, ArkUI!".
this.message = 'Hello, ArkUI!';
})
}
}
}
NOTE
To reference a custom component in another file, use the keyword export to export the component and then use import to import it to the target file.
Multiple HelloComponent instances can be created in build() of other custom components. In this way, HelloComponent is reused across those components.
@Entry
@Component
struct ParentComponent {
build() {
Column() {
// Create HelloComponent multiple times to reuse the custom component.
Text('ArkUI message')
HelloComponent({ message: 'Hello World!' })
Divider()
HelloComponent({ message: 'Hello ArkTS!' })
}
}
}
To fully understand the preceding example, a knowledge of the following concepts is essential:
Basic Structure of a Custom Component
struct
The definition of a custom component must start with the @Component struct followed by the component name, and then component body enclosed by curly brackets. No inheritance is allowed. You can omit the new operator when instantiating a struct.
NOTE
The name assigned to a class, function, or custom component must be different from the name of any built-in component.
@Entry
A custom component decorated with @Entry serves as the entry to a UI page. A single page can only have one @Entry decorated custom component serving as its entry.
NOTE
This decorator can be used in ArkTS widgets since API version 9.
Since API version 10, the @Entry decorator accepts an optional LocalStorage parameter or an optional EntryOptions10+ parameter.
This decorator can be used in atomic services since API version 11.
@Entry
@Component
struct MyComponent {
// ...
}
EntryOptions10+
Describes the named route options.
| Name | Type | Read-Only | Optional | Description |
|---|---|---|---|---|
| routeName | string | No | Yes | Name of the target named route. |
| storage | LocalStorage | No | Yes | Storage of the page-level UI state. If no value is passed, the framework creates a new LocalStorage instance as the default value. |
| useSharedStorage12+ | boolean | No | Yes | Whether to use the LocalStorage instance passed by loadContent. The default value is false. true: Use the shared LocalStorage instance. false: Do not use the shared LocalStorage instance. |
NOTE
When useSharedStorage is set to true and storage is assigned a value, the value of useSharedStorage has a higher priority.
@Entry({ routeName: 'myPage' })
@Component
struct MyComponent {
// ...
}
@Component
The @Component decorated struct is a V1 custom component, which can use the capabilities of state management V1 decorators.
NOTE
This decorator can be used in ArkTS widgets since API version 9.
Since API version 11, @Component can accept a ComponentOptions parameter.
This decorator can be used in atomic services since API version 11.
@Component
struct MyComponent {
// ...
}
@ComponentV2
The @ComponentV2 decorated struct is a V2 custom component, which can use the capabilities of state management V2 decorators.
NOTE
The @ComponentV2 decorator is supported since API version 12.
This decorator can be used in atomic services since API version 12.
This decorator can be used in ArkTS widgets since API version 23.
Similar to the @Component decorator, the @ComponentV2 decorator decorates custom components with the following specifications:
-
In custom components decorated with @ComponentV2, only new state variable decorators can be used, including @Local, @Param, @Once, @Event, @Provider, and @Consumer.
-
Custom components decorated with @ComponentV2 do not support existing component capabilities such as LocalStorage.
-
@ComponentV2 and @Component cannot be used on the same struct.
-
@ComponentV2 supports an optional ComponentOptions parameter to implement the component freezing function.
-
A basic@ComponentV2 decorated custom component should contain the following parts:
@Entry @ComponentV2 // Decorator struct ComponentV2Test { // Struct-declared data structure @Local message: string = 'Hello World'; build() { // UI defined in build RelativeContainer() { Text(this.message) .id('HelloWorld') // Replace $r('app.float.page_text_font_size') with the resource file you use. .fontSize($r('app.float.page_text_font_size')) .fontWeight(FontWeight.Bold) .alignRules({ center: { anchor: '__container__', align: VerticalAlign.Center }, middle: { anchor: '__container__', align: HorizontalAlign.Center } }) .onClick(() => { this.message = 'Welcome'; }) } .height('100%') .width('100%') } }
Unless otherwise specified, a custom component decorated with @ComponentV2 maintains the same behavior as a custom component decorated with @Component.
build()
The build() function is used to define the declarative UI description of a custom component. Every custom component must define a build() function.
@Component
struct MyComponent {
build() {
// ...
}
}
@Reusable
Using @Reusable to decorate V1 custom components makes them reusable. For details, see @Reusable Decorator: Reusing Components.
NOTE
This decorator can be used in ArkTS widgets since API version 10.
@Reusable
@Component
struct MyComponent {
// ...
}
@ReusableV2
Using @ReusableV2 to decorate V2 custom components makes them reusable. For details, see @Reusable V2 Decorator: Reusing V2 Components.
NOTE
This decorator can be used in atomic services since API version 18.
@ReusableV2
@ComponentV2
struct MyComponent {
// ...
}
Member Functions/Variables
In addition to the mandatory build(), a custom component may implement other member functions with the following restrictions:
- Member functions of a custom component can only be accessed from within the component. Avoid declaring them as static functions.
A custom component can also implement member variables with the following restrictions:
-
Member variables of a custom component can only be accessed from within the component. Avoid declaring them as static variables.
-
Local initialization is optional for some member variables and mandatory for others. For details about whether local initialization or initialization from the parent component is required, see State Management.
Rules for Custom Component Parameters
The following example demonstrates how to create a custom component within the build method and initialize its parameters according to decorator rules.
@Component
struct MyComponent {
countDownFrom: number = 0;
color: Color = Color.Blue;
build() {
Column() {
Text(`${this.countDownFrom}`)
.backgroundColor(this.color)
}
}
}
@Entry
@Component
struct ParentComponent {
private someColor: Color = Color.Pink;
build() {
Column() {
// Create an instance of MyComponent and initialize its countDownFrom variable with the value 10 and its color variable with the value this.someColor.
MyComponent({ countDownFrom: 10, color: this.someColor })
}
}
}
In the following example, a function in the parent component is passed to the child component and called therein.
@Entry
@Component
struct Parent {
@State cnt: number = 0;
submit: () => void = () => {
this.cnt++;
};
build() {
Column() {
Text(`${this.cnt}`)
// Pass the function in the parent component to the child component.
Son({ submitArrow: this.submit })
}
}
}
@Component
struct Son {
submitArrow?: () => void;
build() {
Row() {
Button('add')
.width(80)
.onClick(() => {
if (this.submitArrow) {
this.submitArrow()
}
})
}
.height(56)
}
}
build() Implementation Rules
Whatever declared in build() are called UI descriptions. UI descriptions must comply with the following rules:
-
For an @Entry decorated custom component, exactly one root component is required under build(). This root component must be a container component. ForEach is not allowed at the top level. For an @Component decorated custom component, exactly one root component is required under build(). This root component is not necessarily a container component. ForEach is not allowed at the top level.
@Entry @Component struct MyComponent { build() { // Exactly one root component is required, and it must be a container component. Row() { ChildComponent() } } } @Component struct ChildComponent { build() { // Exactly one root component is required, and it is not necessarily a container component. // Replace $r('app.media.startIcon') with the actual resource file. Image($r('app.media.startIcon')) } } -
Local variable declaration is not allowed. The following example should be avoided:
build() { // Avoid: declaring a local variable. let num: number = 1; } -
console.info can be used in the UI description only when it is in a method or function. The following example should be avoided:
build() { // Avoid: using console.info directly in UI description. console.info('print debug log'); } -
Creation of a local scope is not allowed. The following example should be avoided:
build() { // Avoid: creating a local scope. { // ... } } -
Calling a method not decorated by @Builder is not allowed. However, the return value of such methods can be used as parameters of system components. Refer to the code snippet below.
@Component struct ParentComponent { doSomeCalculations() { } build() { Column() { // Avoid: calling a method not decorated by @Builder. this.doSomeCalculations(); } } }@Component struct ParentComponent { calcTextValue(): string { return 'Hello World'; } @Builder doSomeRender() { Text(`Hello World`) } build() { Column() { // Prefer: Call a @Builder decorated method. this.doSomeRender() // Prefer: Pass the return value of a TS method as the parameter. Text(this.calcTextValue()) } } } -
The switch syntax is not allowed. If conditional judgment is required, use the if statement. Refer to the code snippet below.
build() { Column() { // Avoid: using the switch syntax. switch (expression) { case 1: Text('...') break; case 2: Image('...') break; default: Text('...') break; } } }build() { Column() { // Correct usage: Use if. if (this.expression == 1) { Text('...') } else if (this.expression == 2) { Image('...') } else { Text('...') } } } -
Expressions are not allowed except for the if component. Refer to the code snippet below.
build() { Column() { // Avoid: expressions. (this.aVar > 10) ? Text('...') : Image('...') } }build() { Column() { // Positive example: Use if for judgment. if (this.aVar > 10) { Text('...') } else { Image('...') } } } -
Directly changing a state variable is not allowed. The following example should be avoided.
@Component struct MyComponent { @State textColor: Color = Color.Yellow; @State columnColor: Color = Color.Green; @State count: number = 1; build() { Column() { // Avoid directly changing the value of count in the Text component. Text(`${this.count++}`) .width(50) .height(50) .fontColor(this.textColor) .onClick(() => { this.columnColor = Color.Red; }) Button("change textColor").onClick(() =>{ this.textColor = Color.Pink; }) } .backgroundColor(this.columnColor) } }In ArkUI state management, UI re-render is driven by state.

Therefore, do not change any state variable in the build() or @Builder decorated method of a custom component. Otherwise, loop rendering may result. The impact of Text('${this.count++}') varies depending on the update mode:
- Full update (API version 8 and earlier): ArkUI may enter an infinite re-rendering loop because each render of the Text component modifies the application state, triggering another render cycle. When this.columnColor changes, the entire build function is executed. As a result, the text bound to Text(
${this.count++}) also changes. Each time Text(${this.count++}) is re-rendered, the this.count state variable is updated, initiating another build execution and resulting in an infinite loop. - Minimal update (API version 9 and later): Changing this.columnColor updates only the Column component, not the Text component. The entire Text component is updated only when this.textColor changes. During the update, all attribute functions of the component are executed, causing the value of Text(
${this.count++}) to increment. Currently, UI updates occur at the component level. If any attribute of a component changes, the entire component is updated. Therefore, the update chain follows this pattern: this.textColor = Color.Pink -> Text re-render -> this.count++ -> Text re-render. Note that this implementation causes the Text component to render twice during initial rendering, which negatively affects performance.
The behavior of changing the application state in the build function may be more covert than that in the preceding example. The following are some examples:
-
Changing the state variable within the @Builder, @Extend, or @Styles method.
-
Changing the application state variable in the function called during parameter calculation, for example, Text('${this.calcLabel()}')
-
Modifying the current array: In the following code snippet, sort() changes the array this.arr, and the subsequent filter method returns a new array.
// Incorrect usage: @State arr : Array<...> = [ ... ]; ForEach(this.arr.sort().filter(...), item => { // ... })// Prefer: Call filter before sort() to return a new array. In this way, sort() does not change this.arr. ForEach(this.arr.filter((item, index) => index >= 2).sort(), (item: number) => { // ... });
To address this issue, see FAQs appfreeze Due to State Variable Changes in the build Function
- Full update (API version 8 and earlier): ArkUI may enter an infinite re-rendering loop because each render of the Text component modifies the application state, triggering another render cycle. When this.columnColor changes, the entire build function is executed. As a result, the text bound to Text(
Universal Style of a Custom Component
The universal style of a custom component is configured by the chain call.
@Component
struct ChildComponent {
build() {
Button(`Hello World`)
}
}
@Entry
@Component
struct MyComponent {
build() {
Row() {
/ / Property settings to the ChildComponent instead of Button in ChildComponent.
ChildComponent()
.width(200)
.height(300)
.backgroundColor(Color.Red)
}
}
}
NOTE
When applying styles to a custom component (ChildComponent in this example), the ArkUI framework implicitly wraps ChildComponent with an invisible container component. These styles are actually applied to this container component instead of the Button component inside ChildComponent. This behavior can be observed in rendering results: The red background color is not applied directly to the Button component; instead, it is rendered on the invisible container component that wraps the Button component.
Cross-Ability Migration of Custom Components
Before API version 24, custom components did not support cross-ability migration. After a custom component instance moved across abilities, changing its state variables would not trigger a UI component refresh. Note that before the system is upgraded to API version 24, even if enableCustomComponentCrossAbility is set to true in module.json5, this feature will not take effect.
Starting from API version 24, you can enable custom components to support cross-ability migration by configuring the metadata tag in the module.json5 configuration file of your application project. The configuration method is as follows.
"metadata": [
{
"name": "enableCustomComponentCrossAbility",
"value": "true"
}
]
Note:
- You are not advised to asynchronously modify state variables in a migrating component during the onBackground phase of the original ability. At that time, state variables can be assigned, but the refresh of associated components will not be triggered.
- Only custom components in the component tree can be migrated. Custom components that are not attached to the component tree will not be migrated. For example, in scenarios where the OH_ArkUI_GetNodeHandleFromNapiValue parameter is used to obtain an ArkUI_NodeHandle, if the parameter received by OH_ArkUI_GetNodeHandleFromNapiValue is a ComponentContent, the obtained ArkUI_NodeHandle is the first FrameNode node in the subtree under ComponentContent. Any custom components skipped in between will not be part of the component tree and will not support migration.
import { UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
const DOMAIN = 0x0000;
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
onBackground(): void {
// You are not advised to asynchronously modify the state variable in the component to be migrated in the onBackground phase.
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
}
}
- Click Button('add node to tree') to create a BuilderNode and mount it to NodeContainer.
- Click Button('remove node from tree') to remove the BuilderNode from NodeContainer.
- Click Button('start new ability') to start the ExtraAbility.
- Click Button('add node to tree') in ExtraIndex to mount the BuilderNode to the NodeContainer in the ExtraIndex.
- When the custom component ComponentUnderBuilderNode is mounted to a new ability, it instructs the custom component of the ability to update the ID of the ability instance to which it belongs.
- Click Button('change message') in the custom component ComponentUnderBuilderNode to change the value of the state variable message, which triggers the @Watch('messageUpdate') callback and UI refresh.
The following example includes the process of creating a new ability. For details, see starAbility.
import { MyNodeController } from './MyNodeController';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { common, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
const DOMAIN = 0x0000;
@Entry
@Component
struct Index {
private nodeController: MyNodeController = new MyNodeController();
startNewAbility() {
const want: Want = {
bundleName: 'com.example.enablecustomcomponentcrossability',
abilityName: 'ExtraAbility'
};
try {
const context = this.getUIContext()?.getHostContext() as common.UIAbilityContext;
context.startAbility(want, (err: BusinessError) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', `startAbility failed, code is ${err.code}, message is ${err.message}`);
return;
}
hilog.info(DOMAIN, 'testTag', 'startAbility succeed');
});
} catch (err) {
hilog.error(DOMAIN, 'testTag',
`startAbility failed, code is ${(err as BusinessError).code}, message is ${(err as BusinessError).message}`);
}
}
build() {
Column({ space: 10 }) {
Text('Index')
// Create a globalBuilderNode and mount its nodes to the placeholder node of NodeContainer.
Button('add node to tree').width(200).onClick(() => {
this.nodeController.addBuilderNode();
})
// Remove the nodes of globalBuilderNode from the placeholder node of NodeContainer.
Button('remove node from tree').width(200).onClick(() => {
this.nodeController.removeBuilderNode();
})
// Start a new ability.
Button('start new ability').width(200).onClick(() => {
this.startNewAbility();
})
NodeContainer(this.nodeController).backgroundColor('#FFEEF0')
}
.width('100%')
.height('100%')
}
}
import { BuilderNode, FrameNode, NodeController } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN = 0x0000;
let globalBuilderNode: BuilderNode<[]> | undefined = undefined;
export class MyNodeController extends NodeController {
private rootNode: FrameNode | null = null;
private uiContext: UIContext | null = null;
makeNode(uiContext: UIContext): FrameNode | null {
this.rootNode = new FrameNode(uiContext);
this.uiContext = uiContext;
return this.rootNode;
}
addBuilderNode(): void {
if (!globalBuilderNode && this.uiContext) {
globalBuilderNode = new BuilderNode(this.uiContext);
globalBuilderNode.build(wrapBuilder<[]>(buildComponent), undefined);
}
if (this.rootNode && globalBuilderNode) {
this.rootNode.appendChild(globalBuilderNode.getFrameNode());
}
}
removeBuilderNode(): void {
if (this.rootNode && globalBuilderNode) {
this.rootNode.removeChild(globalBuilderNode.getFrameNode());
}
}
disposeNode(): void {
if (this.rootNode && globalBuilderNode) {
globalBuilderNode.dispose();
globalBuilderNode = undefined;
}
}
}
@Builder
function buildComponent() {
Column() {
ComponentUnderBuilderNode()
}
}
@Component
struct ComponentUnderBuilderNode {
@State @Watch('messageUpdate') message: string = 'hello';
messageUpdate() {
hilog.info(DOMAIN, 'testTag', `ComponentUnderBuilderNode message change ${this.message}`);
}
build() {
Column() {
Text(`message: ${this.message}`)
// Change the value of message to trigger the @Watch ('messageUpdate') callback and refresh the Text component.
Button('change message').onClick(() => {
this.message += ' world';
})
}
}
}
import { UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
const DOMAIN = 0x0000;
export default class ExtraAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/ExtraIndex', (err) => {
if (err.code) {
// If ExtraIndex fails to be loaded, an error message is displayed.
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
}
import { MyNodeController } from './MyNodeController';
@Entry
@Component
struct ExtraIndex {
private nodeController: MyNodeController = new MyNodeController();
build() {
Column({ space: 10 }) {
Text('ExtraIndex')
// Attach the nodes under globalBuilderNode as children of the placeholder node of NodeContainer.
Button('add node to tree').width(200).onClick(() => {
this.nodeController.addBuilderNode();
})
// Remove the nodes of globalBuilderNode from the placeholder node of NodeContainer.
Button('remove node from tree').width(200).onClick(() => {
this.nodeController.removeBuilderNode();
})
// Destroy nodes under globalBuilderNode.
Button('dispose node').width(200).onClick(() => {
this.nodeController.disposeNode();
})
NodeContainer(this.nodeController).backgroundColor('#FFEEF0')
}
.width('100%')
.height('100%')
}
}
Constraints
V1 Custom Components Do Not Support Static Code Blocks
Static code blocks are used to initialize static attributes.
-
When you write static code blocks in a custom component decorated with @Component or @CustomDialog, the code will not be executed. From API version 22, the verification of static code blocks is added, and a compilation warning is displayed, indicating that the static code block does not take effect.
@Component struct MyComponent { static a: string = ''; // The static block does not take effect, and the value of a is still an empty string. static { this.a = 'hello world'; } // ... } -
It is supported in the custom component decorated with @ComponentV2.
@ComponentV2 struct MyComponent { static a: string = ''; // The static block takes effect, and the value of a changes to hello world. static { this.a = 'hello world'; } // ... }
Mixing @Component and @ComponentV2
For details about how to mix @Component decorated custom components with @ComponentV2 decorated custom components, see Mixed Use of State Management V1 and V2.