Building a Custom Verification Code Input Component in HarmonyOS ArkUI

Challenges with Multiple TextInput Components

  • Initial Focus: The system does not automatically determine which input field should receive focus upon initialization. This can be partially resolved by adding .defaultFocus(index == 0) to the first input.
  • Deletion Handling: The onChange event often fails to trigger when a user deletes characters (observed on simulators, tablets, and phones). Attempting to use onKeyEvent to listen for the delete key (KeyCode.KEYCODE_DEL) also proves unreliable across devices.
  • Soft Kebyoard Behavior: Using focusControl.requestFocus(nextKeyStr) to move to the next field sometimes causes the soft keyboard to dismiss unexpectedly, leading to a poor user experience.

The Alternative: Single TextInput with Overlayed Text

Due to the limitations above, a more robust solution is to use a single hidden TextInput layered over multiple Text components. The Text components display the code visually, while the invisible TextInput handles the actual user interaction.

Here is the implementation of a CodeInputView component using this strategy:


@Preview
@Component
export struct CodeInputView {
    @State digitCount: number = 4
    inputResultCallback: (string) => void
    @Link userDigits: Array<string>
    @State showCursor: boolean = true
    private inputIdentifier = "hidden_code_field"

    aboutToAppear() {
        if (this.userDigits == null) {
            this.userDigits = new Array(this.digitCount).fill('');
        }
    }

    build() {
        Stack() {
            if (this.userDigits != null) {
                Row({ space: vp(10) }) {
                    ForEach(this.userDigits, (item: string, index: number) => {
                        Text(item)
                            .backgroundColor($r('app.color.white_80'))
                            .height(match())
                            .layoutWeight(1)
                            .fontSize(fp(25))
                            .textAlign(TextAlign.Center)
                            .align(Alignment.Center)
                            .borderRadius(vp(15))
                            .focusable(false)
                            .defaultFocus(false)
                            .focusOnTouch(false)
                            .onClick(() => {
                                focusControl.requestFocus(this.inputIdentifier)
                            })
                    }, (item: string) => item)
                }
                .height(match())
                .width(match())

                TextInput()
                    .maxLength(this.digitCount)
                    .fontSize(fp(25))
                    .borderRadius(vp(15))
                    .type(InputType.Number)
                    .key(this.inputIdentifier)
                    .onChange((value) => {
                        let chars = value.split('')
                        this.userDigits.forEach((val, idx) => {
                            this.userDigits[idx] = chars[idx] || ''
                        })
                        if (chars.length >= this.digitCount) {
                            this.inputResultCallback(value)
                        }
                        this.showCursor = (chars.length == 0)
                    })
                    .copyOption(CopyOptions.None)
                    .caretColor(this.showCursor ? Color.Black : Color.Transparent)
                    .fontColor(Color.Transparent)
                    .backgroundColor(Color.Transparent)
                    .height(match())
                    .width(match())
            }
        }
        .height(vp(80))
    }
}
</string>

Key Implementation Details

  1. Transparent Input: The TextInput is set to fill the entire container with transparent text, background, and cursor (when not needed) to hide it from the user.
  2. Visual Display: ForEach renders a row of Text componants. These act as the visual boxes for the verification code.
  3. Data Binding: In the onChange event of the hidden input, the string is split into an array of characters. These characters are mapped to the userDigits array, which updates the Text components automatically.
  4. Completion Callback: Once the length of the input matches digitCount, the inputResultCallback is triggered to pass the result to the parent component.

To use this component in a parent view:


CodeInputView({
    inputResultCallback: (code) => {
        // Handle the completed verification code
        console.log("Entered code: " + code)
    }
})

Verification Code Component DemoThis approach circumvents the focus and keyboard issues associated with multiple enputs. While there is a minor visual artifact where the background flashes if the state is not perfectly matched, this method provides the most stable experience currently available in ArkUI.

Tags: HarmonyOS ArkUI ArkTS Verification Code Input Component Development

Posted on Mon, 28 Sep 2026 16:18:14 +0000 by youknowho