Animated Masked Text

An animated chip selector where items expand on selection

Last updated on

Edit on GitHub

Manual

Install the following dependencies:

npm install expo-linear-gradient @react-native-masked-view/masked-view

Copy 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