Docs
Avatar

Avatar

Displays an avatar with a fallback option.

JL

Installation

Copy and paste the following code into your component.

import * as React from "react";
import styles from "./avatar.module.css";

const AVATAR_NAME = "Avatar";

type ImageLoadingStatus = "idle" | "loading" | "loaded" | "error";

type AvatarContextValue = {
  imageLoadingStatus: ImageLoadingStatus;
  onImageLoadingStatusChange(status: ImageLoadingStatus): void;
};

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

type SpanProps = React.HTMLAttributes<HTMLSpanElement>;
interface AvatarProps extends SpanProps {}

const Avatar = React.forwardRef<HTMLSpanElement, AvatarProps>(
  (props: AvatarProps, forwardedRef) => {
    const { children, className, ...avatarProps } = props;
    const [imageLoadingStatus, setImageLoadingStatus] =
      React.useState<ImageLoadingStatus>("idle");

    const handleImageLoadingStatusChange = (status: ImageLoadingStatus) => {
      setImageLoadingStatus(status);
    };

    const contextValue: AvatarContextValue = {
      imageLoadingStatus,
      onImageLoadingStatusChange: handleImageLoadingStatusChange,
    };

    return (
      <AvatarContext.Provider value={contextValue}>
        <span
          className={`${styles.avatarBase} ${className ?? ""}`}
          {...avatarProps}
          ref={forwardedRef}>
          {children}
        </span>
      </AvatarContext.Provider>
    );
  }
);

const useAvatarContext = () => {
  const context = React.useContext(AvatarContext);
  if (!context) {
    throw new Error("useAvatar must be used within an AvatarContext Provider");
  }
  return context;
};

Avatar.displayName = AVATAR_NAME;

/* -------------------------------------------------------------------------------------------------
 * AVATAR IMAGE
 * -----------------------------------------------------------------------------------------------*/

const IMAGE_NAME = "AvatarImage";
type ImageProps = React.HTMLAttributes<HTMLImageElement>;

interface AvatarImageProps extends ImageProps {
  src: string;
  alt: string;
}

const AvatarImage = React.forwardRef<HTMLImageElement, AvatarImageProps>(
  (props: AvatarImageProps, forwardedRef) => {
    const { src, alt, className, ...imageProps } = props;
    const context = useAvatarContext();
    const loadingStatus = useImageLoadingStatus(src);

    React.useLayoutEffect(() => {
      context.onImageLoadingStatusChange(loadingStatus);
    }, [context, loadingStatus]);

    return loadingStatus === "loaded" ? (
      <img
        className={`${styles.avatarImage} ${className ?? ""}`}
        {...imageProps}
        ref={forwardedRef}
        src={src}
        alt={alt}
      />
    ) : null;
  }
);

AvatarImage.displayName = IMAGE_NAME;

/* -------------------------------------------------------------------------------------------------
 * AVATAR FALLBACK
 * -----------------------------------------------------------------------------------------------*/

const FALLBACK_NAME = "AvatarFallback";
interface AvatarFallbackProps extends React.HTMLAttributes<HTMLSpanElement> {}

const AvatarFallback = React.forwardRef<HTMLSpanElement, AvatarFallbackProps>(
  (props: AvatarFallbackProps, forwardedRef) => {
    const context = useAvatarContext();
    const { className } = props;
    return context.imageLoadingStatus !== "loaded" ? (
      <span
        className={`${styles.avatarFallback} ${className ?? ""}`}
        {...props}
        ref={forwardedRef}
      />
    ) : null;
  }
);

AvatarFallback.displayName = FALLBACK_NAME;

function useImageLoadingStatus(src?: string): ImageLoadingStatus {
  const [loadingStatus, setLoadingStatus] =
    React.useState<ImageLoadingStatus>("idle");

  React.useLayoutEffect(() => {
    if (!src) {
      setLoadingStatus("error");
      return;
    }

    const image = new window.Image();
    setLoadingStatus("loading");
    image.onload = () => setLoadingStatus("loaded");
    image.onerror = () => setLoadingStatus("error");
    image.src = src;

    return () => {
      image.onload = null;
      image.onerror = null;
    };
  }, [src]);

  return loadingStatus;
}

export { Avatar, AvatarImage, AvatarFallback, useAvatarContext };
export type { AvatarProps, AvatarImageProps, AvatarFallbackProps };

Copy and paste the following CSS into a .module.css file.

.avatarBase {
  border-radius: 50%;
  display: flex;
  flex-shrink: 0;
  height: 2.5rem;
  position: relative;
  overflow: hidden;
  width: 2.5rem;
}

.avatarImage {
  aspect-ratio: 1/1;
  height: 100%;
  width: 100%;
}

.avatarFallback {
  align-items: center;
  border-radius: 50%;
  background: hsl(var(--muted));
  display: flex;
  height: 100%;
  justify-content: center;
  width: 100%;
}

Update the import paths to match your project setup.

Usage

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
<Avatar>
  <AvatarImage src='https://avatars.githubusercontent.com/u/42413866?v=4' />
  <AvatarFallback>JL</AvatarFallback>
</Avatar>