Animated Masked Text
An animated chip selector where items expand on selection
Last updated on
Manual
Install the following dependencies:
npm install expo-linear-gradient @react-native-masked-view/masked-viewCopy and paste the following code into your project.
component/molecules/Chip.tsx
import React, { memo, useEffect, useRef, useState } from "react";import { Animated, Text, StyleSheet, Easing, View } from "react-native";import { LinearGradient } from "expo-linear-gradient";import MaskedView from "@react-native-masked-view/masked-view";import type { IAnimatedMaskedText } from "./AnimatedMaskedText.types";export const AnimatedMaskedText: React.FC<IAnimatedMaskedText> & React.FunctionComponent<IAnimatedMaskedText> = memo<IAnimatedMaskedText>( ({ children, style, speed = 1, colors = [ "transparent", "rgba(255,255,255,0.5)", "rgba(255,255,255,1)", "rgba(255,255,255,0.2)", "transparent", ], baseTextColor = "#000000", }: IAnimatedMaskedText): React.ReactNode & React.JSX.Element & React.ReactElement => { const shimmerTranslate = useRef<Animated.Value>( new Animated.Value(0), ).current; const [textWidth, setTextWidth] = useState<number>(0); const [textHeight, setTextHeight] = useState<number>(0); useEffect(() => { if (textWidth > 0) { shimmerTranslate.setValue(0); Animated.loop( Animated.timing(shimmerTranslate, { toValue: 1, duration: 2500 / speed, easing: Easing.linear, useNativeDriver: true, }), ).start(); } }, [shimmerTranslate, speed, textWidth]); const waveWidth = textWidth * 0.4; const translateX = shimmerTranslate.interpolate<number>({ inputRange: [0, 1], outputRange: [-waveWidth, textWidth + waveWidth], }); return ( <View style={{ position: "relative" }}> <Text style={[styles.text, style, { color: baseTextColor }]} onTextLayout={(e) => { const { width, height } = e.nativeEvent.lines[0]; setTextWidth(width); setTextHeight(height); }} > {children} </Text> {textWidth > 0 && ( <MaskedView style={[ StyleSheet.absoluteFill, { width: textWidth, height: textHeight || 50, }, ]} maskElement={ <Text style={[styles.text, style, { color: "white" }]}> {children} </Text> } > <View style={{ width: textWidth, height: textHeight || 50, backgroundColor: baseTextColor, overflow: "hidden", }} > <Animated.View style={{ transform: [{ translateX }], width: waveWidth, height: textHeight || 50, }} > <LinearGradient colors={colors as any} start={{ x: 0, y: 0 }} end={{ x: 1, y: 0 }} style={{ width: waveWidth, height: textHeight || 50, }} /> </Animated.View> </View> </MaskedView> )} </View> ); },);export default memo< React.FC<IAnimatedMaskedText> & React.FunctionComponent<IAnimatedMaskedText>>(AnimatedMaskedText);const styles = StyleSheet.create({ text: { fontSize: 20, fontWeight: "bold", },});Usage
import { View, Text, StyleSheet } from "react-native";import { GestureHandlerRootView } from "react-native-gesture-handler";import { StatusBar } from "expo-status-bar";import { useFonts } from "expo-font";import { SymbolView } from "expo-symbols";import { ChipGroup } from "@/components";import { useState } from "react";import AnimatedMaskedText from "@/components/molecules/animated-masked-text/AnimatedMaskedText";export default function App() { const [fontLoaded] = useFonts({ SfProRounded: require("@/assets/fonts/sf-pro-rounded.ttf"), Coolveticsa: require("@/assets/fonts/Coolvetica-Rg-Cram.otf"), }); const [selected, setSelected] = useState(0); return ( <GestureHandlerRootView style={styles.container}> <StatusBar style="light" /> <View style={styles.content}> <AnimatedMaskedText style={{ fontSize: 84, fontWeight: "100", }} baseTextColor="#2a2a2a" > Reacticx </AnimatedMaskedText> </View> </GestureHandlerRootView> );}const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#0a0a0a", }, content: { paddingHorizontal: 20, paddingTop: 100, gap: 24, }, title: { fontSize: 34, fontWeight: "700", color: "#fff", },});Props
Expo Linear Gradient
React Native Masked View
