Animated Input Bar
An animated text-input with rotating staggered placeholders
Last updated on
Manual
Install the following dependencies:
npm install react-native-reanimated expo-blurCopy and paste the following code into your project.
component/base/animated-input-bar.tsx
import React, { useState, useEffect, memo } from "react";import { TextInput, View, StyleSheet, TextStyle, StyleProp,} from "react-native";import Animated, { withDelay, withSpring, withTiming, Easing, LinearTransition, useAnimatedProps, useSharedValue, interpolate, withSequence,} from "react-native-reanimated";import { BlurView, type BlurViewProps } from "expo-blur";import type { ICharacter, IAnimatedInput } from "./types";const AnimatedBlurView = Animated.createAnimatedComponent<BlurViewProps>(BlurView);const Character: React.FC<ICharacter> & React.FunctionComponent<ICharacter> = ({ char, index, enterDuration, exitDuration, delayIncrement, style,}: ICharacter) => { const animationDelay = index * delayIncrement; const enteringAnimation = () => { "worklet"; return { initialValues: { opacity: 0, transform: [{ translateY: 20 }, { scale: 0.5 }], }, animations: { opacity: withDelay<number>( animationDelay, withTiming<number>(1, { duration: enterDuration }), ), transform: [ { translateY: withDelay<number>( animationDelay, withSpring<number>(0, { damping: 15, stiffness: 150, mass: 0.9, }), ), }, { scale: withDelay<number>( animationDelay, withSpring<number>(1, { damping: 15, stiffness: 150, mass: 0.9, }), ), }, ], }, }; }; const exitingAnimation = () => { "worklet"; return { initialValues: { opacity: 1, transform: [{ translateY: 0 }, { scale: 1 }], }, animations: { opacity: withDelay( animationDelay, withTiming(0, { duration: exitDuration }), ), transform: [ { translateY: withDelay<number>( animationDelay, withTiming<number>(-5, { duration: exitDuration }), ), }, { scale: withDelay<number>( animationDelay, withTiming<number>(0.5, { duration: exitDuration }), ), }, ], }, }; }; return ( <Animated.Text entering={enteringAnimation} exiting={exitingAnimation} layout={LinearTransition.duration(180).easing( Easing.bezier(0.25, 0.1, 0.25, 1), )} style={style} > {char} </Animated.Text> );};const StaggeredPlaceholder: React.FC<{ text: string; enterDuration: number; exitDuration: number; delayIncrement: number; style?: StyleProp<TextStyle>;}> = ({ text, enterDuration, exitDuration, delayIncrement, style }) => { const characters = Array.from(text); return ( <Animated.View style={styles.placeholderWrapper} layout={LinearTransition.duration(300).easing( Easing.bezier(0.25, 0.1, 0.25, 1), )} > {characters.map((char, index) => ( <Character key={`${char}-${index}-${text}`} char={char} index={index} enterDuration={enterDuration} exitDuration={exitDuration} delayIncrement={delayIncrement} style={style as TextStyle} /> ))} </Animated.View> );};const AnimatedInput: React.FC<IAnimatedInput> & React.FunctionComponent<IAnimatedInput> = memo<IAnimatedInput>( ({ placeholders, animationInterval = 3000, value, onChangeText, containerStyle = { width: "100%", }, inputWrapperStyle, inputStyle, placeholderStyle, characterEnterDuration = 300, characterExitDuration = 200, characterDelayIncrement = 30, blurAnimationDuration = 400, blurIntensityRange = [0, 2.5, 4.5], blurProgressRange = [0, 0.2, 1], ...props }: IAnimatedInput): React.ReactNode & React.JSX.Element & React.ReactElement => { const [isFocused, setIsFocused] = useState<boolean>(false); const [inputValue, setInputValue] = useState<string>(value || ""); const [currentIndex, setCurrentIndex] = useState<number>(0); const blurProgress = useSharedValue<number>(0); useEffect(() => { if (isFocused || inputValue) return; blurProgress.value = withSequence<number>( withTiming<number>(1, { duration: blurAnimationDuration, easing: Easing.bezier(0.25, 0.1, 0.25, 1), }), withTiming<number>(0, { duration: blurAnimationDuration, easing: Easing.bezier(0.25, 0.1, 0.25, 1), }), ); }, [currentIndex, blurAnimationDuration]); useEffect(() => { if (isFocused || inputValue) return; const timeout = setTimeout<[]>(() => { setCurrentIndex((prev) => (prev + 1) % placeholders.length); }, animationInterval); return () => clearTimeout(timeout); }, [ currentIndex, isFocused, inputValue, placeholders.length, animationInterval, ]); const handleChangeText = (text: string) => { setInputValue(text); onChangeText?.(text); }; const animatedBlurViewProps = useAnimatedProps(() => { const intensity = withSpring( interpolate(blurProgress.value, blurProgressRange, blurIntensityRange), ); return { intensity, }; }); return ( <View style={[styles.wrapper, containerStyle]}> <View style={[styles.inputWrapper, inputWrapperStyle]}> {!isFocused && !inputValue && ( <StaggeredPlaceholder text={placeholders[currentIndex]} enterDuration={characterEnterDuration} exitDuration={characterExitDuration} delayIncrement={characterDelayIncrement} // style={[styles.character as any, placeholderStyle as any]} style={[styles.character, placeholderStyle] as any} /> )} <AnimatedBlurView style={[ StyleSheet.absoluteFillObject, { overflow: "hidden", }, ]} animatedProps={animatedBlurViewProps} /> <TextInput style={[styles.input, inputStyle]} value={inputValue} onChangeText={handleChangeText} onFocus={() => setIsFocused(true)} onBlur={() => setIsFocused(false)} placeholderTextColor="transparent" {...props} /> </View> </View> ); },);export default memo< React.FC<IAnimatedInput> & React.FunctionComponent<IAnimatedInput>>(AnimatedInput);const styles = StyleSheet.create({ wrapper: { marginVertical: 8, }, inputWrapper: { paddingHorizontal: 18, paddingVertical: 16, position: "relative", minHeight: 56, justifyContent: "center", }, placeholderWrapper: { position: "absolute", left: 18, flexDirection: "row", flexWrap: "wrap", pointerEvents: "none", }, character: { fontSize: 16, color: "#71717a", fontWeight: "400", }, input: { fontSize: 16, color: "#fafafa", paddingVertical: 0, fontWeight: "400", },});Usage
import { View, StyleSheet, Dimensions } from "react-native";import { GestureHandlerRootView } from "react-native-gesture-handler";import { StatusBar } from "expo-status-bar";import { useFonts } from "expo-font";import { Ionicons } from "@expo/vector-icons";import { useState } from "react";import AnimatedInputBar from "@/components/base/animated-input-bar";const PLACEHOLDERS: string[] = [ "Share your thoughts...", "What's on your mind?", "Type something interesting...", "Express yourself here...",];export default function App() { const [fontLoaded] = useFonts({ SfProRounded: require("@/assets/fonts/sf-pro-rounded.ttf"), }); const [text, setText] = useState<string>(""); return ( <GestureHandlerRootView style={styles.container}> <StatusBar style="light" /> <View style={styles.inputContainer}> <Ionicons name="add" size={18} color={"#fff"} style={{ marginLeft: 10, }} /> <View style={styles.divider} /> <AnimatedInputBar placeholders={PLACEHOLDERS} value={text} animationInterval={900} onChangeText={setText} selectionColor={"#353535"} placeholderStyle={{ fontFamily: fontLoaded ? "SfProRounded" : undefined, }} /> </View> </GestureHandlerRootView> );}const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#000", alignItems: "center", paddingTop: 110, }, inputContainer: { width: Dimensions.get("window").width - 60, flexDirection: "row", alignItems: "center", backgroundColor: "#111", paddingHorizontal: 16, borderRadius: 26, height: 56, }, divider: { width: 0.4, backgroundColor: "#6e6e6e", height: 18, marginLeft: 18, }, btn: { flexDirection: "row", alignItems: "center", gap: 10, backgroundColor: "#fff", paddingVertical: 16, paddingHorizontal: 32, borderRadius: 16, }, btnText: { fontSize: 17, fontWeight: "600", color: "#000", },});Props
ICharacter
React Native Reanimated
Expo Blur
