Avatar
A flexible avatar component
Last updated on
Manual
Install the following dependencies:
npm install react-native-reanimated react-native-workletsCopy and paste the following code into your project.
component/base/avatar.tsx
import React, { useState, useEffect, memo } from "react";import { View, Image, Text, Pressable, StyleSheet, type PressableProps, type ViewStyle,} from "react-native";import Animated, { useSharedValue, useAnimatedStyle, withRepeat, withTiming, interpolate, Easing, LinearTransition,} from "react-native-reanimated";import type { IAvatar } from "./types";const AnimatedPressable = Animated.createAnimatedComponent<PressableProps>(Pressable);export const Avatar: React.FC<IAvatar> & React.FunctionComponent<IAvatar> = memo<IAvatar>( ({ image, size = 40, onPress, showBorder = true, borderColor = "#fff", borderWidth = 2, backgroundColor, textColor = "#fff", disabled = false, loading = false, showAvatar = true, showText = false, textPosition = "bottom", textStyle, shimmerSpeed = 1500, pressedScale = 0.95, pressedOpacity = 0.8, showOnlineIndicator = false, onlineIndicatorColor = "#4CAF50", onlineIndicatorSize, onlineIndicatorBorderWidth, }: IAvatar): | (React.ReactNode & React.JSX.Element & React.ReactElement) | null => { const [hasError, setHasError] = useState<boolean>(false); const [imageLoaded, setImageLoaded] = useState<boolean>(false); const shimmerProgress = useSharedValue<number>(0); const pressScale = useSharedValue<number>(1); const pressOpacity = useSharedValue<number>(1); const fadeOpacity = useSharedValue<number>(loading ? 0 : 1); useEffect(() => { if (loading) { shimmerProgress.value = withRepeat( withTiming<number>(1, { duration: shimmerSpeed, easing: Easing.linear, }), -1, false, ); fadeOpacity.value = 0; } else { shimmerProgress.value = 0; fadeOpacity.value = withTiming(1, { duration: 400, easing: Easing.out(Easing.quad), }); } }, [loading, shimmerSpeed]); const getInitials = <T extends string>(name?: T): string => { if (!name) return "?"; const words = name.trim().split(/\s+/); if (words.length === 1) { return words[0].charAt(0).toUpperCase(); } return ( words[0].charAt(0) + words[words.length - 1].charAt(0) ).toUpperCase(); }; const getBackgroundColor = (): string => { if (backgroundColor) return backgroundColor; if (image.name) { const colors = [ "#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7", "#DDA0DD", "#98D8C8", "#F7DC6F", "#BB8FCE", "#85C1E9", "#FF8A80", "#82B1FF", "#B39DDB", "#A5D6A7", "#FFCC02", ]; let hash = 0; for (let i = 0; i < image.name.length; i++) { hash = image.name.charCodeAt(i) + ((hash << 5) - hash); } return colors[Math.abs(hash) % colors.length]; } return "#ccc"; }; const handleImageLoad = () => { setImageLoaded(true); setHasError(false); }; const handleImageError = () => { setHasError(true); setImageLoaded(false); }; const handlePressIn = () => { if (!disabled && !loading) { pressScale.value = withTiming<number>(pressedScale, { duration: 100, }); pressOpacity.value = withTiming<number>(pressedOpacity, { duration: 100, }); } }; const handlePressOut = () => { if (!disabled && !loading) { pressScale.value = withTiming<number>(1, { duration: 100 }); pressOpacity.value = withTiming<number>(1, { duration: 100 }); } }; const handlePress = () => { if (!disabled && !loading && onPress) { onPress(image.name || image.uri); } }; const shouldShowImage = image.uri && !loading && showAvatar; const avatarSize = { width: size, height: size, borderRadius: size / 2 }; const fontSize = size <= 32 ? size * 0.4 : size * 0.35; const indicatorSize = onlineIndicatorSize || size * 0.25; const shimmerAnimatedStylez = useAnimatedStyle< Pick<ViewStyle, "opacity" | "transform"> >(() => { const translateX = interpolate( shimmerProgress.value, [0, 1], [-size * 1.5, size * 1.5], ); const opacity = interpolate( shimmerProgress.value, [0, 0.5, 1], [0.3, 0.8, 0.3], ); return { transform: [{ translateX }], opacity, }; }); const pressAnimatedStylez = useAnimatedStyle< Pick<ViewStyle, "opacity" | "transform"> >(() => ({ transform: [{ scale: pressScale.value }], opacity: pressOpacity.value, })); const fadeAnimatedStylez = useAnimatedStyle<Pick<ViewStyle, "opacity">>( () => ({ opacity: fadeOpacity.value, }), ); const ShimmerEffect = () => ( <View style={[styles.shimmerContainer, avatarSize]}> <Animated.View style={[ styles.shimmerOverlay, shimmerAnimatedStylez, { width: size * 0.6, height: size, left: -size * 0.3, }, ]} /> </View> ); const AvatarContent: React.FC = (): React.ReactNode & React.ReactElement => { if (loading) { return ( <Animated.View layout={LinearTransition.springify()}> <View style={[ styles.loadingContainer, avatarSize, { backgroundColor: getBackgroundColor() }, ]} > <Text style={[ styles.fallbackText, { fontSize, color: textColor, opacity: 0.6, }, ]} numberOfLines={1} > {getInitials(image.name)} </Text> <ShimmerEffect /> </View> </Animated.View> ); } if (shouldShowImage) { return ( <Animated.View style={[styles.imageContainer, fadeAnimatedStylez]}> <Image source={{ uri: image.uri }} style={[ styles.avatar, avatarSize, showBorder && { borderWidth, borderColor, }, ]} onLoad={() => { console.log("Image loaded!"); handleImageLoad(); }} onError={(e) => { console.error("Image failed to load:", e.nativeEvent); handleImageError(); }} /> {showOnlineIndicator && ( <Animated.View style={[ styles.onlineIndicator, { width: indicatorSize, height: indicatorSize, borderRadius: indicatorSize / 2, backgroundColor: onlineIndicatorColor, borderWidth: onlineIndicatorBorderWidth, right: size * 0.05, bottom: size * 0.05, }, ]} /> )} </Animated.View> ); } return ( <Animated.View style={[styles.fallbackContainer, fadeAnimatedStylez]}> <View style={[ styles.fallback, avatarSize, { backgroundColor: getBackgroundColor() }, showBorder && { borderWidth, borderColor, }, ]} > <Text style={[ styles.fallbackText, { fontSize, color: textColor, }, ]} numberOfLines={1} > {getInitials(image.name)} </Text> </View> {showOnlineIndicator && ( <View style={[ styles.onlineIndicator, { width: indicatorSize, height: indicatorSize, borderWidth: onlineIndicatorBorderWidth, borderRadius: indicatorSize / 2, backgroundColor: onlineIndicatorColor, right: size * 0.05, bottom: size * 0.05, }, ]} /> )} </Animated.View> ); }; const TextContent: React.FC = (): | (React.ReactNode & React.JSX.Element & React.ReactElement) | null => { if (!showText || !image.name) return null; return ( <Text style={[ styles.nameText, textPosition === "right" && styles.nameTextRight, textPosition === "top" && styles.nameTextTop, { fontSize: size * 0.3 }, textStyle, ]} numberOfLines={1} > {image.name} </Text> ); }; return ( <View style={[ styles.container, textPosition === "right" && styles.containerRow, textPosition === "top" && styles.containerColumn, ]} > {textPosition === "top" && <TextContent />} <AnimatedPressable onPress={handlePress} onPressIn={handlePressIn} onPressOut={handlePressOut} disabled={disabled || loading} style={[ styles.pressable, pressAnimatedStylez, disabled && styles.disabled, ]} > <AvatarContent /> </AnimatedPressable> {textPosition === "right" && <TextContent />} {textPosition === "bottom" && <TextContent />} </View> ); }, );const styles = StyleSheet.create({ container: { alignItems: "center", }, containerRow: { flexDirection: "row", alignItems: "center", }, containerColumn: { flexDirection: "column", }, pressable: { borderRadius: 50, }, disabled: { opacity: 0.5, }, imageContainer: { position: "relative", }, avatar: { backgroundColor: "#f0f0f0", }, fallbackContainer: { position: "relative", }, fallback: { justifyContent: "center", alignItems: "center", }, fallbackText: { fontWeight: "600", textAlign: "center", }, loadingContainer: { position: "relative", overflow: "hidden", justifyContent: "center", alignItems: "center", }, shimmerContainer: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, overflow: "hidden", }, shimmerOverlay: { position: "absolute", backgroundColor: "rgba(255, 255, 255, 0.6)", borderRadius: 4, transform: [{ rotate: "20deg" }], }, onlineIndicator: { position: "absolute", borderColor: "#fff", shadowColor: "#000", shadowOffset: { width: 0, height: 1, }, shadowOpacity: 0.2, shadowRadius: 1.5, elevation: 2, }, nameText: { color: "#333", fontWeight: "500", textAlign: "center", marginVertical: 4, }, nameTextRight: { marginLeft: 8, marginVertical: 0, }, nameTextTop: { marginBottom: 6, marginVertical: 0, },});export default memo<React.FC<IAvatar> & React.FunctionComponent<IAvatar>>( Avatar,);Usage
import { StyleSheet, Text, View } from "react-native";import React from "react";import { Avatar } from "@/components";const App = () => { return ( <Avatar image={{ uri: "https://images.pexels.com/photos/2422259/pexels-photo-2422259.jpeg?auto=compress&cs=tinysrgb&w=1200", name: "Avatar Example", }} /> );};export default App;const styles = StyleSheet.create({});Props
IAvatarItem
React Native Reanimated
