Dynamic Text
Animated text that cycles through items with smooth transitions
Last updated on
Manual
Install the following dependencies:
npm install react-native-reanimated expo-blur Copy and paste the following code into your project.
component/molecules/dynamic-text
import React, { useEffect, useState, useCallback, memo } from "react";import { View, Text, StyleSheet, type ViewStyle, type TextStyle, type StyleProp, Platform,} from "react-native";import Animated, { interpolate, LinearTransition, useAnimatedProps, useSharedValue, withTiming,} from "react-native-reanimated";import { BlurView, type BlurViewProps } from "expo-blur";import type { DotConfig, IDynamicText, TextConfig, TimingConfig,} from "./types";import { DEFAULT_TIMING, DEFAULT_DOT, DEFAULT_TEXT } from "./const";import { getAnimationPreset, normalizeItems } from "./helpers";const AnimatedBlurView = Animated.createAnimatedComponent<BlurViewProps>(BlurView);const DynamicText: React.FC<IDynamicText> & React.FunctionComponent<IDynamicText> = memo<IDynamicText>( ({ items, loop = false, loopCount = -1, animationPreset = "fade", animationDirection = "up", customEntering, customExiting, timing, dot, text, containerStyle, contentStyle, onAnimationComplete, onIndexChange, paused = false, initialIndex = 0, accessibilityLabel, }: IDynamicText): React.ReactNode & React.JSX.Element & React.ReactElement & React.ReactChild => { const timingConfig: TimingConfig = { ...DEFAULT_TIMING, ...timing }; const dotConfig: DotConfig = { ...DEFAULT_DOT, ...dot }; const textConfig: TextConfig = { ...DEFAULT_TEXT, ...text }; const normalizedItems = normalizeItems(items); const [currentIndex, setCurrentIndex] = useState<number>(initialIndex); const [isAnimating, setIsAnimating] = useState<boolean>(true); const [currentLoop, setCurrentLoop] = useState<number>(0); const progress = useSharedValue<number>(0); const animationConfig = getAnimationPreset( animationPreset, animationDirection, timingConfig.animationDuration, ); const entering = customEntering ?? animationConfig.entering; const exiting = customExiting ?? animationConfig.exiting; const handleIndexChange = useCallback<(index: number) => void>( <T extends number>(index: T) => { if (onIndexChange && normalizedItems[index]) { onIndexChange(index, normalizedItems[index]); } }, [onIndexChange, normalizedItems], ); useEffect(() => { if (!isAnimating || paused) return; const interval = setInterval(() => { setCurrentIndex((prevIndex: number) => { const nextIndex = prevIndex + 1; if (nextIndex >= normalizedItems.length) { if (loop && (loopCount === -1 || currentLoop < loopCount - 1)) { setCurrentLoop((prev) => prev + 1); handleIndexChange(0); return 0; } clearInterval(interval); setIsAnimating(false); onAnimationComplete?.(); return prevIndex; } handleIndexChange(nextIndex); return nextIndex; }); progress.value = withTiming( 1, { duration: timingConfig.animationDuration }, () => { progress.value = 0; }, ); }, timingConfig.interval); return () => clearInterval(interval); }, [ isAnimating, paused, loop, loopCount, currentLoop, normalizedItems.length, timingConfig.interval, handleIndexChange, onAnimationComplete, progress, ]); const currentItem = normalizedItems[currentIndex]; const dotStyle: StyleProp<ViewStyle> = { height: dotConfig.size, width: dotConfig.size, borderRadius: dotConfig.size / 2, backgroundColor: dotConfig.color as string, ...dotConfig?.style, }; const textStyle: StyleProp<TextStyle> = { fontSize: textConfig.fontSize, fontWeight: textConfig.fontWeight, color: textConfig.color as string, ...textConfig.style, }; const animatedBlurViewPropz = useAnimatedProps< Pick<BlurViewProps, "intensity"> >(() => { const blurIntensity = interpolate( progress.value, [0, 0.5, 1], [0, 10, 0], ); return { intensity: blurIntensity, }; }); return ( <View style={[styles.container, containerStyle]} accessibilityLabel={accessibilityLabel} accessibilityRole="text" > <View style={[styles.textContainer, contentStyle]}> <Animated.View key={`${currentItem.id}-${currentLoop}`} entering={entering} exiting={exiting} layout={LinearTransition} style={styles.content} > {dotConfig.visible && <View style={dotStyle} />} <Text style={textStyle}>{currentItem.text}</Text> {Platform.OS === "ios" && ( <AnimatedBlurView animatedProps={animatedBlurViewPropz} style={[StyleSheet.absoluteFillObject]} /> )} </Animated.View> </View> </View> ); },);const styles = StyleSheet.create({ container: { minHeight: 200, alignItems: "center", justifyContent: "center", padding: 16, }, textContainer: { height: 64, width: 240, alignItems: "center", justifyContent: "center", overflow: "hidden", }, content: { position: "absolute", flexDirection: "row", alignItems: "center", gap: 8, },});export { DynamicText };export default memo< React.FC<IDynamicText> & React.FunctionComponent<IDynamicText>>(DynamicText);Usage
import { DynamicText } from "@/components/molecules/dynamic-text";import { DynamicTextItem } from "@/components/molecules/dynamic-text/types";import * as React from "react";import { StyleSheet, View } from "react-native";import { GestureHandlerRootView } from "react-native-gesture-handler";const greetings: readonly DynamicTextItem[] = [ { text: "Hello", id: "en" }, { text: "こんにちは", id: "ja" }, { text: "Bonjour", id: "fr" }, { text: "Hola", id: "es" }, { text: "안녕하세요", id: "ko" },] as const;export default function App(): React.JSX.Element & React.ReactNode { return ( <GestureHandlerRootView style={{ flex: 1 }}> <View style={styles.container}> <DynamicText items={greetings} initialIndex={2} paused={false} loop dot={{ size: 5, }} /> </View> </GestureHandlerRootView> );}const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#131313ff", alignItems: "center", justifyContent: "center", },});Props
DynamicTextItem
React Native Reanimated
Expo Blur
