Sitemap
Gowtham J

Gowtham J

React Native Developer | 3.5+ Years | Built 15+ Mobile Apps | Firebase • Full Stack | Scalable App Development

Trending Flying Text Animation in React Native with Reanimated

6 min readMay 13, 2026

--

Watch full tutorial:
https://youtu.be/789zlhlpyrQ

In this tutorial, we are building a trending flying text animation in React Native using Reanimated.

Instead of showing text normally, every character flies from a random position and smoothly lands in place.

The best part is:

  • Works with real typing
  • Supports replay animation
  • Includes blinking cursor
  • Fully customizable

Final Output

We are building this:

<FlyingTextInput
value={focusGoal}
onChangeText={setFocusGoal}
placeholder="Type your focus goal…"
fontSize={24}
/>

This creates a fully animated input where every typed character flies into position.

Step 1: Create Animated Character Component

First, create a reusable animated character component.

function AnimatedChar({
char,
index = 0,
delay = 0,
fontSize = 22,
color = '#fff',
isLive = false,
trigger = 0,
}: {
char: string;
index?: number;
delay?: number;
fontSize?: number;
color?: string;
isLive?: boolean;
trigger?: any;
}) {

This component handles:

  • single character animation
  • random movement
  • spring animation
  • opacity and scale

Step 2: Generate Random Flying Position

Now create random positions for every character.

const offset = useRef({
x: (Math.random() - 0.5) * (isLive ? 250 : 300),
y: (Math.random() - 0.5) * (isLive ? 400 : 500),
r: (Math.random() - 0.5) * (isLive ? 90 : 120),
}).current;

Here:

  • x → horizontal movement
  • y → vertical movement
  • r → rotation angle

This makes every character fly differently.

Step 3: Animate Characters with Reanimated

Now animate the letters using withSpring.

useEffect(() => {
progress.value = 0;
progress.value = withDelay(
isLive ? 0 : index * delay,
withSpring(1, {
damping: 14,
stiffness: 120,
mass: 0.5,
})
);
}, [trigger, isLive, index, delay]);

This creates:

  • stagger animation
  • smooth spring movement
  • sequential character animation

Step 4: Apply Flying Animation Styles

Now connect animation values to styles.

const animatedStyle = useAnimatedStyle(() => {
const p = progress.value;
const inv = 1 - p;
return {
opacity: p,
transform: [
{ translateX: inv * offset.x },
{ translateY: inv * offset.y },
{ rotate: `${inv * offset.r}deg` },
{ scale: (isLive ? 0.4 : 0.5) + p * (isLive ? 0.6 : 0.5) },
],
};
});

This is the main flying effect.

Each character:

  • starts from random position
  • rotates
  • scales
  • fades in
  • lands smoothly

Step 5: Render Animated Characters

Now render the animated text.

return (
<Animated.View style={animatedStyle}>
<Text
style={[
styles.char,
{
fontSize,
color,
lineHeight: fontSize * 1.4,
},
]}>
{char}
</Text>
</Animated.View>
);

Every character is rendered separately.

Get Gowtham J’s stories in your inbox

Join Medium for free to get updates from this writer.

That is why the animation looks realistic.

Step 6: Build FlyingText Component

Now create the static text animation component.

export function FlyingText({
text,
fontSize = 20,
delay = 30,
color = '#fff',
}: {
text: string;
fontSize?: number;
delay?: number;
color?: string;
}) {

Split the text into characters:

{text.split('').map((c, i) => (
<AnimatedChar
key={`${key}-${i}`}
char={c}
index={i}
delay={delay}
fontSize={fontSize}
color={color}
trigger={key}
/>
))}

Now every letter animates independently.

Step 7: Add Replay Animation

Replay button:

<Pressable
onPress={() => setKey(k => k + 1)}
style={styles.button}>
<Text style={styles.buttonText}>Replay</Text>
</Pressable>

Changing the key forces all characters to animate again.

Step 8: Create FlyingTextInput

Now build the live animated input.

export function FlyingTextInput({
value: propValue,
onChangeText,
fontSize = 24,
color = '#fff',
placeholder = 'Start typing…',
}) {

This component:

  • captures keyboard input
  • animates every typed letter
  • supports replay and clear actions

Step 9: Create Hidden TextInput

The real keyboard input is hidden.

<TextInput
ref={inputRef}
value={val}
onChangeText={t => {
propValue === undefined
? setInternalValue(t)
: onChangeText?.(t);
}}
style={styles.hiddenInput}
autoFocus
autoCorrect={false}
autoCapitalize="none"
/>

Users type normally,
but we render animated text separately.

Step 10: Animate Typed Characters

Now render live animated letters.

{val.split('').map((c, i) => (
<AnimatedChar
key={charKeys[i] ?? i}
char={c}
fontSize={fontSize}
color={color}
isLive
/>
))}

Every typed character:

  • gets random position
  • flies into place
  • animates independently

Step 11: Add Blinking Cursor

Create animated cursor.

useEffect(() => {
cursorOpacity.value = withRepeat(
withTiming(0, { duration: 500 }),
-1,
true
);
}, []);

Render cursor:

<Animated.View
style={[
styles.cursor,
{ height: fontSize * 1.2 },
cursorStyle,
]}
/>

Now the input feels real.

Step 12: Final Usage

import React, { useEffect, useRef, useState } from 'react';
import { Text, View, StyleSheet, Pressable, TextInput } from 'react-native';
import Animated, { useAnimatedStyle, useSharedValue, withDelay, withSpring, withRepeat, withTiming } from 'react-native-reanimated';

// ─── Shared Animated Character ───────────────────────────────────

function AnimatedChar({ char, index = 0, delay = 0, fontSize = 22, color = '#fff', isLive = false, trigger = 0,
}: {
char: string;
index?: number;
delay?: number;
fontSize?: number;
color?: string;
isLive?: boolean;
trigger?: any;
}) {

const progress = useSharedValue(0);
const offset = useRef({
x: (Math.random() - 0.5) * (isLive ? 250 : 300),
y: (Math.random() - 0.5) * (isLive ? 400 : 500),
r: (Math.random() - 0.5) * (isLive ? 90 : 120),
}).current;

useEffect(() => {
progress.value = 0;
progress.value = withDelay(
isLive ? 0 : index * delay,
withSpring(1, { damping: 14, stiffness: 120, mass: 0.5 })
);
}, [trigger, isLive, index, delay]);

const animatedStyle = useAnimatedStyle(() => {
const p = progress.value;
const inv = 1 - p;
return {
opacity: p,
transform: [
{ translateX: inv * offset.x },
{ translateY: inv * offset.y },
{ rotate: `${inv * offset.r}deg` },
{ scale: (isLive ? 0.4 : 0.5) + p * (isLive ? 0.6 : 0.5) },
],
};
});

if (char === ' ') return <View style={{ width: fontSize * 0.3 }} />;

return (
<Animated.View style={animatedStyle}>
<Text style={[styles.char, { fontSize, color, lineHeight: fontSize * 1.4 }]}>
{char}
</Text>
</Animated.View>
);
}

// ─── FlyingTextInput (Interactive) ────────────────────────────────

export function FlyingTextInput({ value: propValue, onChangeText, fontSize = 24, color = '#fff', placeholder = 'Start typing…' }: { value?: string; onChangeText?: (t: string) => void; fontSize?: number; color?: string; placeholder?: string }) {

const [internalValue, setInternalValue] = useState('');
const val = propValue ?? internalValue;
const inputRef = useRef<TextInput>(null);
const [charKeys, setCharKeys] = useState<number[]>([]);
const nextKey = useRef(0);
const cursorOpacity = useSharedValue(1);

useEffect(() => {
cursorOpacity.value = withRepeat(withTiming(0, { duration: 500 }), -1, true);
}, []);

useEffect(() => {
setCharKeys(prev => {
if (val.length > prev.length) {
return [...prev, ...Array(val.length - prev.length).fill(0).map(() => nextKey.current++)];
}
return prev.slice(0, val.length);
});
}, [val]);

const cursorStyle = useAnimatedStyle(() => ({ opacity: cursorOpacity.value }));

return (
<View style={styles.cardContainer}>
<Text style={styles.cardTitle}>Focus Goal</Text>
<Pressable style={styles.inputBox} onPress={() => inputRef.current?.focus()}>
{!val && <Text style={[styles.placeholder, { fontSize: fontSize * 0.8 }]}>{placeholder}</Text>}
<View style={styles.charContainer}>
{val.split('').map((c, i) => (
<AnimatedChar key={charKeys[i] ?? i} char={c} fontSize={fontSize} color={color} isLive />
))}
<Animated.View style={[styles.cursor, { height: fontSize * 1.2 }, cursorStyle]} />
</View>
</Pressable>
<TextInput
ref={inputRef}
value={val}
onChangeText={t => { propValue === undefined ? setInternalValue(t) : onChangeText?.(t); }}
style={styles.hiddenInput}
autoFocus autoCorrect={false} autoCapitalize="none"
/>
<View style={styles.cardActions}>
<Pressable onPress={() => setCharKeys(prev => prev.map(() => nextKey.current++))} style={styles.button}>
<Text style={styles.buttonText}>Replay</Text>
</Pressable>
{val.length > 0 && (
<Pressable onPress={() => { propValue === undefined ? setInternalValue('') : onChangeText?.(''); inputRef.current?.focus(); }} style={styles.clearButton}>
<Text style={styles.clearButtonText}>Clear</Text>
</Pressable>
)}
</View>
</View>
);
}

const styles = StyleSheet.create({
wrapper: { width: '100%', paddingVertical: 16, alignItems: 'center' },
cardContainer: {
width: '100%',
backgroundColor: 'rgba(255, 255, 255, 0.04)',
borderRadius: 32,
padding: 24,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.08)',
},
cardTitle: {
color: 'rgba(255, 255, 255, 0.4)',
fontSize: 10,
fontWeight: '900',
textTransform: 'uppercase',
letterSpacing: 2,
marginBottom: 16,
textAlign: 'center',
},
textBox: {
width: '100%',
minHeight: 100,
backgroundColor: 'rgba(255, 255, 255, 0.03)',
borderRadius: 20,
padding: 20,
justifyContent: 'center',
},
inputBox: {
width: '100%',
minHeight: 120,
backgroundColor: 'rgba(0, 0, 0, 0.2)',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
borderRadius: 20,
padding: 20,
justifyContent: 'center',
},
charContainer: { flexDirection: 'row', flexWrap: 'wrap', alignItems: 'center' },
char: { fontWeight: '700' },
placeholder: { color: 'rgba(255, 255, 255, 0.2)', fontWeight: '500', position: 'absolute', left: 20 },
cursor: { width: 2, backgroundColor: '#3b82f6', borderRadius: 2, marginLeft: 2 },
hiddenInput: { position: 'absolute', width: 1, height: 1, opacity: 0 },
cardActions: { flexDirection: 'row', justifyContent: 'center', gap: 12, marginTop: 24 },
button: { backgroundColor: '#fff', paddingVertical: 10, paddingHorizontal: 20, borderRadius: 12 },
buttonText: { color: '#000', fontWeight: '800', fontSize: 12, textTransform: 'uppercase' },
clearButton: { backgroundColor: 'rgba(255, 255, 255, 0.05)', paddingVertical: 10, paddingHorizontal: 20, borderRadius: 12 },
clearButtonText: { color: 'rgba(255, 255, 255, 0.6)', fontWeight: '700', fontSize: 12, textTransform: 'uppercase' },
});

Conclusion

This animation works because:

  • every character is animated separately
  • each letter has random movement
  • Reanimated handles everything on the UI thread

The result feels smooth, interactive, and modern.

Perfect for:

  • onboarding screens
  • AI apps
  • portfolio projects
  • futuristic UI designs

Full tutorial video:
https://youtu.be/789zlhlpyrQ

--

--

Gowtham J
Gowtham J

Written by Gowtham J

React Native Developer | 3.5+ Years | Built 15+ Mobile Apps | Firebase • Full Stack | Scalable App Development