Smooth Border Text Input
TextInput with a label and animated border that smoothly transitions colors based on focus, error state, and value presence, with optional start icon and error message display.
react-nativetextinputformanimationreanimatedui-componentinputvalidation
Installation
SmoothBorderTextInput.tsx
import {
View,
TextInput,
StyleSheet,
StyleProp,
ViewStyle,
TextInputProps,
Text,
PixelRatio
} from "react-native";
import Animated, {
useAnimatedStyle,
useSharedValue,
interpolateColor,
withTiming,
Easing,
ReduceMotion,
} from "react-native-reanimated";
import { useRef } from "react";
const DEFAULT_INPUT_HEIGHT = 50; //you can change this according to your liking
type SmoothBorderTextInputProps = {
containerStyle?: StyleProp<ViewStyle>;
backgroundColor?: string;
label: string;
labelColor: string; // label color which is the top
valueColor: string; // input value color
isFocusBorderColor: string; // border color while editing
isBlurBorderColor: string; //border color when there is no text value
isBlurValueBorderColor: string; //border color when you finish entering the text
startIcon?: React.ReactElement;
isError?: boolean;
errorMessage?: string;
reduceMotion?: "never" | "always" | "system";
};
export default function SmoothBorderTextInput(
props: React.JSX.IntrinsicAttributes &
React.JSX.IntrinsicClassAttributes<TextInput> &
Readonly<TextInputProps> &
SmoothBorderTextInputProps
) {
const inputRef = useRef<TextInput>(null);
const fontScale = PixelRatio.getFontScale();
const animatedValue = useSharedValue(0);
const motion =
props?.reduceMotion === "never"
? ReduceMotion.Never
: props?.reduceMotion === "always"
? ReduceMotion.Always
: ReduceMotion.System;
// Handle focus and blur events
const handleFocus = () => {
animatedValue.value = withTiming(1, {
duration: 350,
easing: Easing.in(Easing.linear),
reduceMotion: motion,
});
};
const handleBlur = () => {
animatedValue.value = withTiming(0, {
duration: 250,
easing: Easing.out(Easing.linear),
reduceMotion: motion,
});
};
const BorderStyle = useAnimatedStyle(() => {
// Define the "from" color (unfocused state)
let fromColor;
// Define the "to" color (focused state)
let toColor;
if (props.isError) {
// Error state - always red regardless of focus
fromColor = "#F65936";
toColor = "#F65936";
} else {
// No error state - handle normal cases
if (props.value) {
// Has value
fromColor = props?.isBlurValueBorderColor;
} else {
// No value
fromColor = props?.isBlurBorderColor;
}
// Focus color is always the same
toColor = props.isFocusBorderColor;
}
return {
borderColor: interpolateColor(
animatedValue.value,
[0, 1],
[fromColor, toColor]
),
zIndex: 1,
};
});
return (
<View style={{ marginBottom: 16 }}>
<Text style={[styles.label, { color: props?.labelColor}]}>
{props?.label}
</Text>
<Animated.View
// onTouchStart={() => inputRef?.current?.focus()}
style={[
styles.container,
{
backgroundColor: props?.backgroundColor ?? "transparent",
height: DEFAULT_INPUT_HEIGHT * fontScale,
},
BorderStyle,
]}
>
{!!props?.startIcon && (
<View style={styles.iconContainer}>{props.startIcon}</View>
)}
<TextInput
ref={inputRef}
clearButtonMode="while-editing"
placeholderTextColor={props?.labelColor}
style={[
styles.input,
{
color: props?.valueColor,
},
props?.style,
]}
onFocus={handleFocus}
onBlur={handleBlur}
{...props}
/>
</Animated.View>
{props?.isError && (
<Text style={[styles.errorText, { color: "#F65936" }]}>
{props?.errorMessage}
</Text>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: "row",
// position: "relative",
alignItems: "center",
marginBlock: 4,
borderWidth: 1,
borderRadius: 12,
},
input: {
flex: 1,
fontSize: 14,
borderRadius: 12,
padding: 12,
height: "100%",
outline: "none",
},
label: {
fontSize: 14,
marginLeft: 8,
zIndex: 100,
},
errorText: {
fontSize: 12,
},
iconContainer: {
zIndex: 2
},
});
Usage
import React, { useState } from 'react';
import { View, StyleSheet, SafeAreaView, Text } from 'react-native';
import SmoothBorderTextInput from './SmoothBorderTextInput';
import { useAppColors } from '@/hooks/useAppColors';
const App = () => {
const [inputValue, setInputValue] = useState('');
const colors = useAppColors();
return (
<SafeAreaView style={[styles.safeArea, { backgroundColor: colors.BackgroundSystem }]}>
<View style={styles.container}>
<Text style={[styles.title, { color: colors.Neutral900 }]}>
My Awesome Input
</Text>
<SmoothBorderTextInput
label="Your Name"
value={inputValue}
onChangeText={setInputValue}
placeholder="e.g., Jane Doe"
labelColor={colors.Neutral500}
valueColor={colors.Neutral700}
isFocusBorderColor={colors.PrimaryNormal}
isBlurBorderColor={colors.Neutral100}
isBlurValueBorderColor={colors.SuccessfulNormal}
/>
<Text style={[styles.displayValue, { color: colors.Neutral700 }]}>
You typed: {inputValue}
</Text>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
safeArea: {
flex: 1,
},
container: {
flex: 1,
padding: 20,
justifyContent: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
textAlign: 'center',
},
displayValue: {
marginTop: 20,
fontSize: 16,
textAlign: 'center',
},
});
export default App;
Props
Prop | Type | Default | Required | Description |
---|---|---|---|---|
label | string | Yes | The text to display as a label above the input field. | |
labelColor | string | Yes | Color of the label text. | |
valueColor | string | Yes | Color of the text entered into the input field. | |
isFocusBorderColor | string | Yes | Border color of the input field when it is focused (being edited). | |
isBlurBorderColor | string | Yes | Border color when the input is not focused and has no text value. | |
isBlurValueBorderColor | string | Yes | Border color when the input is not focused but has a text value. | |
containerStyle | StyleProp<ViewStyle> | No | Custom styles for the main container wrapping the input and icon. | |
backgroundColor | string | 'transparent' | No | Background color for the input field's container. |
startIcon | React.ReactElement | No | An optional icon component to display at the beginning of the input field. | |
isError | boolean | false | No | If true, displays the input in an error state (e.g., red border). |
errorMessage | string | No | An error message to display below the input when isError is true. | |
reduceMotion | 'never' | 'always' | 'system' | 'system' | No | Controls animation behavior (border color transition). |
...props | TextInputProps | Varies | Inherits all other props from the standard React Native TextInput . |