Unstable Curved Text Marquee
Curved looping text marquee
Last updated on
Manual
Install the following dependencies:
npm install react-native-reanimated react-native-svgCopy and paste the following code into your project.
component/organisms/unstable_curved-text-marquee
/** * 🎨 Original Inspiration: reactbits.dev * Optimized Curved Marquee using Reanimated pattern */import React, { useMemo, memo } from "react";import { Dimensions, StyleSheet, View } from "react-native";import Animated, { useSharedValue, useAnimatedProps, useFrameCallback, type FrameInfo,} from "react-native-reanimated";import Svg, { Defs, Path, Text, TextPath, TextPathProps,} from "react-native-svg";import type { ICurvedLoop } from "./types";const SCREEN_WIDTH = Dimensions.get("window").width;const AnimatedTextPath = Animated.createAnimatedComponent(TextPath);export const UnStableCurvedLoopMarquee: React.FC<ICurvedLoop> = memo( ({ text: marqueeText = "React Native + Expo + SVG ❤️", speed = 500, curve = 400, direction = "left", textColor = "#ffffff", fontSize = 96, copies = 50, style, }: ICurvedLoop): React.ReactElement => { const offset = useSharedValue<number>(0); const text = useMemo<string>(() => { const hasTrailing = /\s|\u00A0$/.test(marqueeText); return ( (hasTrailing ? marqueeText.replace(/\s+$/, "") : marqueeText) + "\u00A0" ); }, [marqueeText]); const spacing = useMemo(() => { return text.length * 2 * (fontSize * 1.6); }, [text, fontSize]); const pathId = useMemo( () => `curved-path-${Math.random().toString(36).slice(2)}`, [], ); const pathD = useMemo(() => `M-100,40 Q500,${40 + curve} 1540,40`, [curve]); const totalText = useMemo(() => { const numCopies = Math.max(copies, Math.ceil(1800 / spacing) + 2); return Array(numCopies).fill(text).join(""); }, [text, spacing, copies]); useFrameCallback((frameInfo: FrameInfo) => { "worklet"; if (spacing === 0) return; const deltaTime = frameInfo.timeSincePreviousFrame ?? 16; const distance = (speed * deltaTime) / 1000; if (direction === "left") { offset.value -= distance; if (offset.value <= -spacing) { offset.value += spacing; } } else { offset.value += distance; if (offset.value >= 0) { offset.value -= spacing; } } }, spacing > 0); const animatedProps = useAnimatedProps<Pick<TextPathProps, "startOffset">>( () => { "worklet"; return { startOffset: offset.value, }; }, ); if (spacing === 0) { return <View style={styles.container} />; } return ( <View style={[ styles.container, style ?? { height: 120, overflow: "hidden", }, ]} > <Svg width="100%" height="100%" viewBox="0 0 1440 120" style={styles.svg} key={curve} > <Defs> <Path id={pathId} d={pathD} fill="none" stroke="transparent" /> </Defs> <Text fill={textColor} fontSize={fontSize}> <AnimatedTextPath href={`#${pathId}`} animatedProps={animatedProps}> {totalText} </AnimatedTextPath> </Text> </Svg> </View> ); },);const styles = StyleSheet.create({ container: { width: "100%", }, svg: { overflow: "visible", },});export default UnStableCurvedLoopMarquee;Usage
import React from "react";import { StyleSheet, Text, View } from "react-native";import { StatusBar } from "expo-status-bar";import { SymbolView } from "expo-symbols";import { UnstableAurora } from "@/components/organisms/unstable_aurora";import { useFonts } from "expo-font";import UnStableCurvedLoopMarquee from "@/components/organisms/unstable_curved-text-marquee";import { PulsingDots } from "@/components";export default function HomeScreen() { const [fontLoaded] = useFonts({ SfProRounded: require("@/assets/fonts/sf-pro-rounded.ttf"), HelveticaNowDisplay: require("@/assets/fonts/HelveticaNowDisplayMedium.ttf"), }); return ( <View style={styles.container}> <StatusBar style="light" /> <View style={{ width: "100%", top: 50, position: "absolute", }} > <UnStableCurvedLoopMarquee text={`✦ React Native`} fontSize={120} /> </View> </View> );}const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#000", alignItems: "center", }, marqueeWrapper: { alignSelf: "stretch", marginHorizontal: 20, paddingVertical: 12, borderRadius: 20, backgroundColor: "rgba(255,255,255,0.04)", // optional – VERY subtle depth shadowColor: "#000", shadowOpacity: 0.15, shadowRadius: 20, shadowOffset: { width: 0, height: 8 }, overflow: "hidden", },});Props
React Native Reanimated
React Native Svg
