Glow
An animated glow outline that wraps any element
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/base/glow.tsx
import React, { type ReactNode, useEffect, useState, memo } from "react";import { View, type LayoutChangeEvent, StyleSheet, ViewStyle,} from "react-native";import Svg, { Defs, LinearGradient, Stop, Rect, type RectProps, type LinearGradientProps,} from "react-native-svg";import Animated, { useSharedValue, useAnimatedProps, withRepeat, withTiming, withSequence, Easing, interpolate, type SharedValue,} from "react-native-reanimated";import type { AnimationStyle, GlowProps, Layout, GradientStop } from "./types";const AnimatedRect = Animated.createAnimatedComponent<RectProps>(Rect);const AnimatedLinearGradient = Animated.createAnimatedComponent<LinearGradientProps>(LinearGradient);const getBorderRadius = (children: ReactNode): number | undefined => { if (!React.isValidElement(children)) return undefined; const child = children as React.ReactElement<{ style?: ViewStyle }>; const style = child.props?.style; if (!style) return undefined; if (Array.isArray(style)) { for (const s of style) { const flattened = StyleSheet.flatten(s); if (flattened?.borderRadius !== undefined) { return flattened.borderRadius as number; } } } else { const flattened = StyleSheet.flatten(style); if (flattened?.borderRadius !== undefined) { return flattened.borderRadius as number; } } return undefined;};export const Glow: React.FC<GlowProps> & React.FunctionComponent<GlowProps> = memo<GlowProps>( ({ children, size = 4, color = "#8b5cf6", secondaryColor = "#ec4899", duration = 3000, style = "pulse", radius: radiusProp, intensity = 1, speed = 1, enabled = true, animated = true, gradient, width = 20, }): React.ReactNode & React.JSX.Element & React.ReactElement => { const [layout, setLayout] = useState<Layout>({ width: 0, height: 0 }); const progress: SharedValue<number> = useSharedValue<number>(0); const detectedRadius = getBorderRadius(children); const radius: number = radiusProp ?? detectedRadius ?? 20; useEffect(() => { if (!enabled || !animated) { progress.value = 0; return; } const adjustedDuration = duration / speed; const animations: Record<AnimationStyle, any> = { linear: withRepeat( withTiming(1, { duration: adjustedDuration, easing: Easing.linear, }), -1, false, ), withoutEasing: withRepeat( withTiming(1, { duration: adjustedDuration }), -1, false, ), pulse: withRepeat( withTiming(1, { duration: adjustedDuration, easing: Easing.bezier(0.45, 0, 0.55, 1), }), -1, false, ), wave: withRepeat( withTiming(1, { duration: adjustedDuration, easing: Easing.inOut(Easing.ease), }), -1, false, ), breathe: withRepeat( withSequence( withTiming(1, { duration: adjustedDuration / 2, easing: Easing.inOut(Easing.quad), }), withTiming(0, { duration: adjustedDuration / 2, easing: Easing.inOut(Easing.quad), }), ), -1, false, ), snap: withRepeat( withTiming(1, { duration: adjustedDuration, easing: Easing.bezier(0.68, -0.55, 0.265, 1.55), }), -1, false, ), spinner: withRepeat( withTiming(1, { duration: adjustedDuration, easing: Easing.linear, }), -1, false, ), }; progress.value = animations[style]; }, [duration, style, speed, enabled, animated, progress]); const handleLayout = (event: LayoutChangeEvent): void => { const { width, height } = event.nativeEvent.layout; setLayout({ width, height }); }; const animatedGradient = useAnimatedProps(() => { if (!animated) { return { x1: "0%", y1: "0%", x2: "100%", y2: "100%", }; } const angle = progress.value * Math.PI * 2; return { x1: `${50 + Math.cos(angle) * 50}%`, y1: `${50 + Math.sin(angle) * 50}%`, x2: `${50 + Math.cos(angle + Math.PI) * 50}%`, y2: `${50 + Math.sin(angle + Math.PI) * 50}%`, }; }); const animatedOpacity = useAnimatedProps(() => { if (!animated) { return { opacity: intensity }; } const baseOpacity = style === "breathe" ? progress.value : interpolate(progress.value, [0, 0.5, 1], [0.6, 1, 0.6]); return { opacity: baseOpacity * intensity }; }); if (!layout.width || !layout.height) { return <View onLayout={handleLayout}>{children}</View>; } const maxRadius = Math.min(layout.width, layout.height) / 2; const actualRadius = Math.min(radius, maxRadius); const glowRadius = actualRadius + size / 2; const getStops = (): ReadonlyArray<GradientStop> => { if (style === "spinner") { const half = width / 2; return [ { offset: "0%", color, opacity: 0 }, { offset: `${Math.max(0, 50 - half - 10)}%`, color, opacity: 0 }, { offset: `${Math.max(0, 50 - half)}%`, color, opacity: 0.3 }, { offset: "50%", color: secondaryColor, opacity: 1 }, { offset: `${Math.min(100, 50 + half)}%`, color, opacity: 0.3 }, { offset: `${Math.min(100, 50 + half + 10)}%`, color, opacity: 0 }, { offset: "100%", color, opacity: 0 }, ]; } return [ { offset: "0%", color, opacity: 0 }, { offset: "25%", color, opacity: 1 }, { offset: "50%", color: secondaryColor, opacity: 1 }, { offset: "75%", color, opacity: 1 }, { offset: "100%", color, opacity: 0 }, ]; }; const stops = gradient ?? getStops(); return ( <View style={{ position: "relative" }}> {enabled && ( <View style={{ position: "absolute", top: -size, left: -size, right: -size, bottom: -size, }} pointerEvents="none" > <Svg width={layout.width + size * 2} height={layout.height + size * 2} > <Defs> <AnimatedLinearGradient id="gradient" animatedProps={animatedGradient} > {stops.map((stop, index) => ( <Stop key={index} offset={stop.offset} stopColor={stop.color} stopOpacity={stop.opacity} /> ))} </AnimatedLinearGradient> </Defs> <AnimatedRect x={size / 2} y={size / 2} width={layout.width + size} height={layout.height + size} rx={glowRadius} ry={glowRadius} fill="none" stroke="url(#gradient)" strokeWidth={size} animatedProps={animatedOpacity} /> </Svg> </View> )} <View onLayout={handleLayout}>{children}</View> </View> ); }, );export default memo<React.FC<GlowProps> & React.FunctionComponent<GlowProps>>( Glow,);Usage
import { View, StyleSheet, Text } from "react-native";import { GestureHandlerRootView } from "react-native-gesture-handler";import { StatusBar } from "expo-status-bar";import Glow from "@/components/base/glow";import { useFonts } from "expo-font";import { Octicons } from "@expo/vector-icons";export default function App() { const [fontLoaded] = useFonts({ SfProRounded: require("@/assets/fonts/sf-pro-rounded.ttf"), HelveticaNowDisplay: require("@/assets/fonts/HelveticaNowDisplayMedium.ttf"), }); return ( <GestureHandlerRootView style={styles.container}> <StatusBar style="light" /> <Glow size={2} style="withoutEasing" intensity={1} speed={0.9} duration={8000} gradient={[ { offset: "5%", color: "#ffc130", opacity: 0.9 }, { offset: "10%", color: "#f5b111", opacity: 1 }, { offset: "50%", color: "transparent", opacity: 0 }, ]} > <View style={styles.card}> <Octicons name="sparkles-fill" size={18} color="#f5b111" /> <Text style={[ styles.text, { fontFamily: fontLoaded ? "HelveticaNowDisplay" : undefined, }, ]} > Generate </Text> </View> </Glow> </GestureHandlerRootView> );}const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#0a0a0a", alignItems: "center", marginTop: 100, }, card: { width: 280, height: 56, backgroundColor: "#151515", borderRadius: 28, justifyContent: "center", alignItems: "center", flexDirection: "row", gap: 8, }, text: { fontSize: 17, fontWeight: "600", color: "#fff", letterSpacing: 0.5, },});Props
GradientStop
React Native Reanimated
React Native Svg
