Avatar

PreviousNext

A avatar component for React Native applications.

Docs
nativeuiui

Preview

Loading preview…
registry/avatar/avatar.tsx
import * as React from "react";
import {
  Image,
  View,
  Text,
  ImageSourcePropType,
  ImageStyle,
} from "react-native";
import { cn } from "@/lib/utils";

interface AvatarRootProps {
  className?: string;
  children: React.ReactNode;
}

const AvatarRoot = React.forwardRef<View, AvatarRootProps>(
  ({ className, children, ...props }, ref) => {
    return (
      <View
        ref={ref}
        className={cn(
          "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
          className
        )}
        {...props}
      >
        {children}
      </View>
    );
  }
);

interface AvatarImageProps {
  className?: string;
  source: ImageSourcePropType;
  alt?: string;
  style?: ImageStyle;
  onLoad?: () => void;
  onError?: () => void;
}

const AvatarImage = React.forwardRef<Image, AvatarImageProps>(
  ({ className, source, alt, ...props }, ref) => {
    const imageContext = React.useContext(AvatarContext);

    const handleError = () => {
      imageContext?.setHasError(true);
      props.onError?.();
    };

    const handleLoad = () => {
      imageContext?.setImageLoaded(true);
      props.onLoad?.();
    };

    return (
      <Image
        ref={ref}
        source={source}
        accessibilityLabel={alt}
        className={cn("h-full w-full object-cover", className)}
        onError={handleError}
        onLoad={handleLoad}
        {...props}
      />
    );
  }
);

interface AvatarFallbackProps {
  className?: string;
  children: React.ReactNode;
  delayMs?: number;
}

interface AvatarContextValue {
  hasError: boolean;
  setHasError: React.Dispatch<React.SetStateAction<boolean>>;
  imageLoaded: boolean;
  setImageLoaded: React.Dispatch<React.SetStateAction<boolean>>;
}

const AvatarContext = React.createContext<AvatarContextValue | null>(null);

const AvatarFallback = React.forwardRef<View, AvatarFallbackProps>(
  ({ className, children, delayMs = 600, ...props }, ref) => {
    const [isShowing, setIsShowing] = React.useState(delayMs === 0);
    const avatarContext = React.useContext(AvatarContext);

    React.useEffect(() => {
      if (delayMs === 0) return;

      // Only show fallback if image has error or hasn't loaded after delay
      const timer = setTimeout(() => {
        if (!avatarContext?.imageLoaded) {
          setIsShowing(true);
        }
      }, delayMs);

      return () => clearTimeout(timer);
    }, [delayMs, avatarContext?.imageLoaded]);

    // Hide fallback if image loads successfully
    React.useEffect(() => {
      if (avatarContext?.imageLoaded) {
        setIsShowing(false);
      }
    }, [avatarContext?.imageLoaded]);

    if (!isShowing || avatarContext?.imageLoaded) {
      return null;
    }

    return (
      <View
        ref={ref}
        className={cn(
          "absolute inset-0 flex h-full w-full items-center justify-center bg-muted",
          className
        )}
        {...props}
      >
        {typeof children === "string" ? (
          <Text className="text-base font-medium text-muted-foreground">
            {children}
          </Text>
        ) : (
          children
        )}
      </View>
    );
  }
);

// Wrap the original AvatarRoot to provide context
const Avatar = React.forwardRef<View, AvatarRootProps>((props, ref) => {
  const [hasError, setHasError] = React.useState(false);
  const [imageLoaded, setImageLoaded] = React.useState(false);

  return (
    <AvatarContext.Provider
      value={{
        hasError,
        setHasError,
        imageLoaded,
        setImageLoaded,
      }}
    >
      <AvatarRoot ref={ref} {...props} />
    </AvatarContext.Provider>
  );
});

AvatarRoot.displayName = "AvatarRoot";
Avatar.displayName = "Avatar";
AvatarImage.displayName = "AvatarImage";
AvatarFallback.displayName = "AvatarFallback";

export { Avatar, AvatarImage, AvatarFallback };

Installation

npx shadcn@latest add @nativeui/avatar

Usage

import { Avatar } from "@/components/ui/avatar"
<Avatar />