The making of a floating-label input with React Native's Animated API

August 15, 2017 0 Comments

The making of a floating-label input with React Native's Animated API

 

 

15 Aug 2017

The making of a floating-label input with React Native's Animated API

We’ve all seen inputs like this:

The label is big and looks like a placeholder… until you focus in the input, then it gets smaller and moves up.

It looks gorgeous.
Smooth.
Flawless.

It also feels like something only an experienced developer would be able to make, right?

Well, it might have been true before React Native, when people lived in cages and did all sorts of weird shit.
Not anymore, though.

You can watch the screencast or keep reading the post, depending on which you prefer most :)


There are two cases we are dealing with.

The first one is when the input is not focused:

The label appears inside the input field, and its size is the size of the text field.
The color dims.
It seems similar to what the placeholder property will give.

And the second case is when the input is focused:

The label appears above the input, the text is smaller, and the text color is different.

The simplest implementation

We can finally start getting out hands dirty and make the simplest possible implementation.
Without any animations just yet.

It appears we have two UI states:

  1. Input is not focused, the label is inside the input.
  2. Input is focused, the label is above the input.

We could, essentially, have a state of whether an input is focused.
Then, depending on this piece of state, we could choose where to place the label, and which styles to apply to it.

Since the label can be in different places and we don’t want it to affect the layout of the component, we will absolutely position the label.
To make sure it has space, we will also add a top padding to the wrapping view.

class FloatingLabelInput extends Component { state = { isFocused: false, }; handleFocus = () => this.setState({ isFocused: true }); handleBlur = () => this.setState({ isFocused: false }); render() { const { label, ...props } = this.props; const { isFocused } = this.state; const labelStyle = { position: 'absolute', left: 0, top: !isFocused ? 18 : 0, fontSize: !isFocused ? 20 : 14, color: !isFocused ? '#aaa' : '#000', }; return ( <View style={{ paddingTop: 18 }}> <Text style={labelStyle}> {label} </Text> <TextInput {...props} style={{ height: 26, fontSize: 20, color: '#000', borderBottomWidth: 1, borderBottomColor: '#555' }} onFocus={this.handleFocus} onBlur={this.handleBlur} /> </View> ); } 
}
<FloatingLabelInput label="Email" value={this.state.value} onChange={this.handleTextChange} 
/>

And with that, we are able to achieve this:

Which is a great starting point.
We don’t have any animations just yet, but other than that, we can change where we put the label depending on whether the input has focus.

Aside: why not use placeholder?

It could be tempting to want to use the placeholder property of TextInput. However, this will not work, because we want to control how, when, and where the label is rendered.

Instead, what we want is to have the label placed inside the text field when not focused. And we want to have it move up then the input is focused — and we can only achieve continuity if that is the same element.


What about ‘em animations?

Actually, this is the easiest part.

Since we have two states that the label can be in, and already pick one based on focus status, animating that transition between the two states is pretty trivial.

Referring to the building blocks of React Native animations, we can identify the following:

  • the animated number will represent whether the field is focused (1) or not (0)
  • we will gradually transition that number to 1 when focusing, and to 0 when blurring
  • we will express the styling of the label in terms of that animated number: we will define the styles at breakpoints (0 and 1), and RN will calculate and apply the intermediate styles automatically. Even for color!

By the way, if you want to have the building blocks handy in the form of a PDF cheatsheet, you can get it right now:

Article continues:

Implementing these is not hard, either.

For the animated number itself, we will need to initialize it in componentWillMount.

componentWillMount() { this.animatedIsFocused = new Animated.Value(0); 
}

Then, since the value of this number should be based on whether the input is focused, and since we already have that bit of information in state, we can add a componentDidUpdate function which will transition this number depending on this.state:

componentDidUpdate() { Animated.timing(this.animatedIsFocused, { toValue: this.state.isFocused ? 1 : 0, duration: 200, }).start(); 
}

Now, to express the styling of the label in terms of it, we’ll only need two changes:

Switch to Animated.Text.

Instead of using a conditional to pick the styles, define them in the following way:

const labelStyle = { position: 'absolute', left: 0, top: this.animatedIsFocused.interpolate({ inputRange: [0, 1], outputRange: [18, 0], }), fontSize: this.animatedIsFocused.interpolate({ inputRange: [0, 1], outputRange: [20, 14], }), color: this.animatedIsFocused.interpolate({ inputRange: [0, 1], outputRange: ['#aaa', '#000'], }), 
};

If you try to type something into the demo above, and blur the input, you’ll see something odd happen.

Luckily, the fix is pretty simple.
We only need to update two lines of code to fix that.

We want to check whether the value of the input is empty, and transition to the “unfocused” state only if both conditions hold true:

  • the value is empty;
  • the input is not focused.

Otherwise we want to show the “focused” style with the label above the input.

And since the input is controlled, we can access its value pretty easily using this.props.

componentWillMount() { this.animatedIsFocused = new Animated.Value(this.props.value === '' ? 0 : 1); 
} componentDidUpdate() { Animated.timing(this._animatedIsFocused, { toValue: (this.state.isFocused || this.props.value !== '') ? 1 : 0, duration: 200, }).start();
}


Tag cloud