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/onBlurand (optionally) the focus controller to manage focus transitions. -
Explicit animation: Use per-component
.animation({...})so style/position/opacity updates animate smoothly.
Implementation Steps
-
Stack layout: Place a
TextInputand aText(used as the custom placeholder) inside aStackso they can overlap. - State variables: Track placeholder font size, top margin, opacity, height/width to represent “resting” vs “floating” states.
-
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.
- On focus, call
-
Animation: Attach
.animation({...})to the placeholder container and/or text so transitions are smooth. -
Reusability: Wrap this behavior in a reusable
InputComponentthat acceptsplaceholder,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
}
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
- TextInput: https://developer.huawei.com/consumer/en/doc/harmonyos-references/ts-basic-components-textinput
- Focus control: https://developer.huawei.com/consumer/en/doc/harmonyos-references/ts-universal-attributes-focus
- Explicit animation: https://developer.huawei.com/consumer/en/doc/harmonyos-references/ts-explicit-animation
Top comments (0)