A React and Next.js avatar component with a real fallback

Build an <Avatar> that shows a user's real picture from a username, email, or domain, and falls back to initials when there isn't one.

Most avatar components fake it: a colored circle with initials, because finding the real picture is someone else's problem. This one shows the real picture when it exists and falls back to initials when it does not, with no backend.

The component

import { useState } from "react"

type AvatarProps = {
  /** e.g. "github/torvalds", "email/jane@stripe.com", "domain/stripe.com" */
  lookup: string
  name: string
  size?: number
}

export function Avatar({ lookup, name, size = 40 }: AvatarProps) {
  const [failed, setFailed] = useState(false)
  const initials = name
    .split(/\s+/)
    .map((part) => part[0] ?? "")
    .join("")
    .slice(0, 2)
    .toUpperCase()

  if (failed) {
    return (
      <span
        aria-label={name}
        style={{
          width: size,
          height: size,
          borderRadius: "50%",
          display: "inline-grid",
          placeItems: "center",
          background: "#e9e4d8",
          fontSize: size * 0.4,
          fontWeight: 600
        }}
      >
        {initials}
      </span>
    )
  }

  return (
    <img
      src={`https://avatarsapi.com/${lookup}?size=${size * 2}`}
      alt={name}
      width={size}
      height={size}
      loading="lazy"
      style={{ borderRadius: "50%", objectFit: "cover" }}
      onError={() => setFailed(true)}
    />
  )
}

Usage:

<Avatar lookup="github/torvalds" name="Linus Torvalds" />
<Avatar lookup="email/jane@stripe.com" name="Jane Doe" />
<Avatar lookup="domain/stripe.com" name="Stripe" size={24} />

Why this works: avatarapi returns a real 404 when nobody has a picture for that Handle, instead of a default silhouette with a 200. So onError fires exactly when there is no real image. Requesting size * 2 keeps it sharp on high-density screens.

Next.js <Image>

If you prefer next/image, allow the host in next.config.js:

module.exports = {
  images: {
    remotePatterns: [{ protocol: "https", hostname: "avatarsapi.com" }]
  }
}

Then swap <img> for <Image> and keep the same onError. Consider unoptimized: avatarapi already resizes (?size=) and caches at the edge, so routing the image through Next's optimizer adds a second fetch and a second cache without making it smaller.

Using a Project key

On a free or paid Project, prefix the path with your publishable key. pk_ keys are safe in client code:

src={`https://avatarsapi.com/p/${process.env.NEXT_PUBLIC_AVATARS_KEY}/${lookup}?size=${size * 2}`}

Free use, anonymous or on a free Project, requires a visible "Avatars by avatarsapi.com" link where the images appear; Starter removes that. Details are in the docs.

Related

Try it: put https://avatarsapi.com/github/torvalds in an <img src>. No key, no signup. Read the docs or create a Project.