Last active
November 10, 2023 07:31
-
-
Save halilb/9ac8e43e95ffbda42d52c34d420e78a4 to your computer and use it in GitHub Desktop.
A material TextField with React Native https://bilir.me/blog/creating-an-animated-textfield-with-react-native
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState } from 'react' | |
import { StyleSheet, Text, ScrollView, Button } from 'react-native' | |
import TextField from './components/TextField' | |
export default function App() { | |
const [value, setValue] = useState('') | |
const [error, setError] = useState<string | null>(null) | |
return ( | |
<ScrollView contentContainerStyle={styles.content}> | |
<Text style={styles.title}>Payment details</Text> | |
<TextField | |
style={styles.textField} | |
value={value} | |
label="Cardholder name" | |
errorText={error} | |
onChangeText={(text) => setValue(text)} | |
/> | |
<Button | |
title="Set error" | |
onPress={() => setError('This field is required.')} | |
/> | |
</ScrollView> | |
) | |
} | |
const styles = StyleSheet.create({ | |
content: { | |
paddingTop: 96, | |
paddingHorizontal: 36, | |
}, | |
title: { | |
fontFamily: 'Avenir-Heavy', | |
color: 'black', | |
fontSize: 32, | |
fontWeight: 'bold', | |
marginBottom: 32, | |
}, | |
textField: { | |
marginBottom: 32, | |
}, | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect, useRef, useState } from 'react' | |
import { | |
Text, | |
TextInput, | |
StyleSheet, | |
View, | |
Animated, | |
Easing, | |
TouchableWithoutFeedback, | |
} from 'react-native' | |
type Props = React.ComponentProps<typeof TextInput> & { | |
label: string | |
errorText?: string | null | |
} | |
const TextField: React.FC<Props> = (props) => { | |
const { | |
label, | |
errorText, | |
value, | |
style, | |
onBlur, | |
onFocus, | |
...restOfProps | |
} = props | |
const [isFocused, setIsFocused] = useState(false) | |
const inputRef = useRef<TextInput>(null) | |
const focusAnim = useRef(new Animated.Value(0)).current | |
useEffect(() => { | |
Animated.timing(focusAnim, { | |
toValue: isFocused || !!value ? 1 : 0, | |
duration: 150, | |
easing: Easing.bezier(0.4, 0, 0.2, 1), | |
useNativeDriver: true, | |
}).start() | |
}, [focusAnim, isFocused, value]) | |
let color = isFocused ? '#080F9C' : '#B9C4CA' | |
if (errorText) { | |
color = '#B00020' | |
} | |
return ( | |
<View style={style}> | |
<TextInput | |
style={[ | |
styles.input, | |
{ | |
borderColor: color, | |
}, | |
]} | |
ref={inputRef} | |
{...restOfProps} | |
value={value} | |
onBlur={(event) => { | |
setIsFocused(false) | |
onBlur?.(event) | |
}} | |
onFocus={(event) => { | |
setIsFocused(true) | |
onFocus?.(event) | |
}} | |
/> | |
<TouchableWithoutFeedback onPress={() => inputRef.current?.focus()}> | |
<Animated.View | |
style={[ | |
styles.labelContainer, | |
{ | |
transform: [ | |
{ | |
scale: focusAnim.interpolate({ | |
inputRange: [0, 1], | |
outputRange: [1, 0.75], | |
}), | |
}, | |
{ | |
translateY: focusAnim.interpolate({ | |
inputRange: [0, 1], | |
outputRange: [24, -12], | |
}), | |
}, | |
{ | |
translateX: focusAnim.interpolate({ | |
inputRange: [0, 1], | |
outputRange: [16, 0], | |
}), | |
}, | |
], | |
}, | |
]} | |
> | |
<Text | |
style={[ | |
styles.label, | |
{ | |
color, | |
}, | |
]} | |
> | |
{label} | |
{errorText ? '*' : ''} | |
</Text> | |
</Animated.View> | |
</TouchableWithoutFeedback> | |
{!!errorText && <Text style={styles.error}>{errorText}</Text>} | |
</View> | |
) | |
} | |
const styles = StyleSheet.create({ | |
input: { | |
padding: 24, | |
borderWidth: 1, | |
borderRadius: 4, | |
fontFamily: 'Avenir-Medium', | |
fontSize: 16, | |
}, | |
labelContainer: { | |
position: 'absolute', | |
paddingHorizontal: 8, | |
backgroundColor: 'white', | |
}, | |
label: { | |
fontFamily: 'Avenir-Heavy', | |
fontSize: 16, | |
}, | |
error: { | |
marginTop: 4, | |
marginLeft: 12, | |
fontSize: 12, | |
color: '#B00020', | |
fontFamily: 'Avenir-Medium', | |
}, | |
}) | |
export default TextField |
It's been two years, but a hack to solve @iaminarush issue is to modify the translationX into
translateX: focusAnim.interpolate({
inputRange: [0, 1],
outputRange: [16, 16 - props.label.length],
})
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The label's position onFocus position is different if the label text have different width, demo here