DEV Community

HarmonyOS
HarmonyOS

Posted on

How to implement a text input field where the placeholder text automatically moves to the top-left corner when it gains focus

Read the original article:How to implement a text input field where the placeholder text automatically moves to the top-left corner when it gains focus

How to implement a text input field where the placeholder text automatically moves to the top-left corner when it gains focus

Requirement Description

Implement a “floating label” effect for TextInput: when the field gains focus, the placeholder shrinks and moves to the top-left corner of the input; when the field loses focus and is empty, the placeholder returns to its original centered position.

Background Knowledge

  • TextInput: Single-line text input component used for user text entry.
  • Focus control: Use onFocus / onBlur and (optionally) the focus controller to manage focus transitions.
  • Explicit animation: Use per-component .animation({...}) so style/position/opacity updates animate smoothly.

Implementation Steps

  1. Stack layout: Place a TextInput and a Text (used as the custom placeholder) inside a Stack so they can overlap.
  2. State variables: Track placeholder font size, top margin, opacity, height/width to represent “resting” vs “floating” states.
  3. Focus handlers:
    • On focus, call textMin() to shrink and move the label to the top-left; optionally request focus to the input by ID.
    • On blur, call textMax() to restore the label only if the input value is empty.
  4. Animation: Attach .animation({...}) to the placeholder container and/or text so transitions are smooth.
  5. Reusability: Wrap this behavior in a reusable InputComponent that accepts placeholder, inputId, and width hints.

Code Snippet / Configuration

@Entry
@Component
struct Index {
  build() {
    Column() {
      Blank().height(50)
      InputComponent({ placeholder: 'Name',  inputId: '1', originWight: 40 })
      Blank().height(25)
      InputComponent({ placeholder: 'Phone', inputId: '2', originWight: 40 })
      Blank().height(25)
      InputComponent({ placeholder: 'Residence', inputId: '3', originWight: 40 })
      Blank().height(25)
      InputComponent({ placeholder: 'Home Address', inputId: '4', originWight: 70 })
    }
    .height('100%')
    .width('100%')
  }
}

@Component
struct InputComponent {
  text: string = ''
  inputId: string | undefined = undefined
  controller: TextInputController = new TextInputController()
  placeholder: string = ''
  @State textFontSize: number = 20
  @State textHeight: number = 40
  originWight: number = 0
  @State textWeight: number = 40
  @State textMarginTop: number = 10
  @State textOpacity: number = 0.5
  textBorderWidth: number = 2
  textRadius: number = 10
  @State isFocus: boolean = false

  build() {
    Stack({ alignContent: Alignment.TopStart }) {
      TextInput({})
        .id(this.inputId)
        .fontSize(20)
        .width(300)
        .height(40)
        .margin({ top: 10 })
        .opacity(0.5)
        .backgroundColor(Color.White)
        .defaultFocus(this.isFocus)
        .border({
          width: this.textBorderWidth,
          color: Color.Black,
          radius: this.textRadius,
          style: BorderStyle.Solid
        })
        .onFocus(() => { textMin(this) })
        .onBlur(() => { textMax(this) })
        .onChange((value: string) => { this.text = value })

      Column() {
        Text(this.placeholder)
          .fontSize(this.textFontSize * 0.8)
          .width(this.originWight !== 0 ? this.originWight : this.textWeight)
          .height(this.textHeight - this.textBorderWidth * 2)
          .opacity(this.textOpacity)
          .borderRadius(10)
          .textAlign(TextAlign.Center)
          .focusable(false)
          .renderFit(RenderFit.CENTER)
          .animation({
            duration: 1000,
            curve: Curve.Ease,
            iterations: 1,
            playMode: PlayMode.Normal
          })
          .onClick(() => { textMin(this) })
          .onBlur(() => { textMax(this) })
      }
      .borderRadius(10)
      .margin({
        top: this.textMarginTop + this.textBorderWidth,
        left: this.textBorderWidth * 3,
        bottom: this.textBorderWidth
      })
      .backgroundColor(Color.White)
      .animation({
        duration: 1000,
        curve: Curve.Ease,
        iterations: 1,
        playMode: PlayMode.Normal
      })
    }
  }
}

// Shrink & lift the placeholder when focused
function textMin(com: InputComponent) {
  com.textFontSize = 12
  com.textMarginTop = 0
  com.textOpacity = 1
  com.textHeight = 20
  com.textWeight = 35
  com.getUIContext().getFocusController().requestFocus(com.inputId)
}

// Restore placeholder when blurred AND empty
function textMax(com: InputComponent) {
  if (com.text !== '') return
  com.textFontSize = 20
  com.textMarginTop = 10
  com.textOpacity = 0.5
  com.textHeight = 40
  com.textWeight = com.originWight
}
Enter fullscreen mode Exit fullscreen mode

Test Results

  • On focus, the placeholder smoothly shrinks and moves to the top-left of the input; on blur with empty value, it returns to center.
  • Typing text keeps the label in its floating position after blur (no visual jump).
  • Clicking the label also triggers focus and animation.

Limitations or Considerations

  • The example uses fixed sizes for simplicity; for responsive layouts, bind sizes/margins to container width or use relative units.
  • Ensure animation durations/curves are tuned for accessibility and UX; consider reduced-motion settings if applicable.
  • If you use multiple inputs in forms with keyboard navigation, verify focus order and label click behavior across all fields.

Related Documents or Links

Written by Bunyamin Eymen Alagoz

Top comments (0)