avatar

PreviousNext
Docs
takiui

Preview

Loading preview…
registry/new-york/ui/avatar.tsx
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"

type AvatarContextValue = {
  imageLoadingStatus: "idle" | "loading" | "loaded" | "error"
  onImageLoadingStatusChange: (status: "loading" | "loaded" | "error") => void
}

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

function useAvatarContext() {
  const context = React.useContext(AvatarContext)
  if (!context) {
    throw new Error("Avatar components must be used within Avatar")
  }
  return context
}

interface AvatarProps extends React.HTMLAttributes<HTMLSpanElement> {}

function Avatar({ className, ...props }: AvatarProps) {
  const [imageLoadingStatus, setImageLoadingStatus] = React.useState<
    "idle" | "loading" | "loaded" | "error"
  >("idle")

  return (
    <AvatarContext.Provider
      value={{
        imageLoadingStatus,
        onImageLoadingStatusChange: setImageLoadingStatus,
      }}
    >
      <span
        data-slot="avatar"
        className={cn(
          "relative flex size-8 shrink-0 overflow-hidden rounded-full",
          className
        )}
        {...props}
      />
    </AvatarContext.Provider>
  )
}

interface AvatarImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {}

function AvatarImage({ className, src, alt, ...props }: AvatarImageProps) {
  const { onImageLoadingStatusChange } = useAvatarContext()
  const [loadingStatus, setLoadingStatus] = React.useState<
    "idle" | "loading" | "loaded" | "error"
  >("idle")

  React.useEffect(() => {
    if (!src) {
      onImageLoadingStatusChange("error")
      return
    }

    let isMounted = true
    const image = new window.Image()

    const updateStatus = (status: "loading" | "loaded" | "error") => {
      if (!isMounted) return
      setLoadingStatus(status)
      onImageLoadingStatusChange(status)
    }

    updateStatus("loading")

    image.onload = () => updateStatus("loaded")
    image.onerror = () => updateStatus("error")
    image.src = src

    return () => {
      isMounted = false
    }
  }, [src, onImageLoadingStatusChange])

  if (loadingStatus !== "loaded") {
    return null
  }

  return (
    <img
      data-slot="avatar-image"
      className={cn("aspect-square size-full", className)}
      src={src}
      alt={alt}
      {...props}
    />
  )
}

interface AvatarFallbackProps extends React.HTMLAttributes<HTMLSpanElement> {
  delayMs?: number
}

function AvatarFallback({ className, delayMs, ...props }: AvatarFallbackProps) {
  const { imageLoadingStatus } = useAvatarContext()
  const [canRender, setCanRender] = React.useState(delayMs === undefined)

  React.useEffect(() => {
    if (delayMs === undefined) return

    const timerId = window.setTimeout(() => setCanRender(true), delayMs)
    return () => window.clearTimeout(timerId)
  }, [delayMs])

  if (!canRender || imageLoadingStatus === "loaded") {
    return null
  }

  return (
    <span
      data-slot="avatar-fallback"
      className={cn(
        "bg-muted flex size-full items-center justify-center rounded-full",
        className
      )}
      {...props}
    />
  )
}

export { Avatar, AvatarImage, AvatarFallback }

Installation

npx shadcn@latest add @taki/avatar

Usage

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