React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

avatarprofileimagecomponenttailwind

Avatar

Displays a user's profile image with an initials fallback when no image is available, commonly used in navigation bars, comment threads, and user lists.

Use Cases

  • Display user profile pictures in navigation headers
  • Show commenter avatars in discussion threads
  • Represent team members in a project dashboard
  • Display sender photos in chat or messaging interfaces
  • Show participants in a video call lobby
  • Indicate online/offline status alongside user identity
  • Stack overlapping avatars to represent a group of users

Simplest Implementation

"use client";
 
interface AvatarProps {
  src?: string;
  name: string;
}
 
export function Avatar({ src, name }: AvatarProps) {
  const initials = name
    .split(" ")
    .map((n) => n[0])
    .join("")
    .slice(0, 2)
    .toUpperCase();
 
  return src ? (
    <img
      src={src}
      alt={name}
      className="h-10 w-10 rounded-full object-cover"
    />
  ) : (
    <div className="flex h-10 w-10 items-center justify-center rounded-full bg-blue-600 text-sm font-medium text-white">
      {initials}
    </div>
  );
}

Renders an image when src is provided, otherwise falls back to initials extracted from the user's name. The object-cover class prevents image distortion on non-square photos.

Variations

Image Avatar with Fallback on Error

"use client";
 
import { useState } from "react";
 
interface AvatarProps {
  src?: string;
  name: string;
}
 
export function Avatar({ src, name }: AvatarProps) {
  const [failed, setFailed] = useState(false);
 
  const initials = name
    .split(" ")
    .map((n) => n[0])
    .join("")
    .slice(0, 2)
    .toUpperCase();
 
  if (!src || failed) {
    return (
      <div className="flex h-10 w-10 items-center justify-center rounded-full bg-blue-600 text-sm font-medium text-white">
        {initials}
      </div>
    );
  }
 
  return (
    <img
      src={src}
      alt={name}
      onError={() => setFailed(true)}
      className="h-10 w-10 rounded-full object-cover"
    />
  );
}

Gracefully handles broken image URLs by catching the onError event and falling back to initials. Without this, a broken image shows the browser's default broken icon.

Initials with Color Variants

"use client";
 
interface AvatarProps {
  name: string;
}
 
const colors = [
  "bg-red-600",
  "bg-orange-600",
  "bg-amber-600",
  "bg-green-600",
  "bg-teal-600",
  "bg-blue-600",
  "bg-indigo-600",
  "bg-purple-600",
  "bg-pink-600",
];
 
function getColorFromName(name: string): string {
  let hash = 0;
  for (let i = 0; i < name.length; i++) {
    hash = name.charCodeAt(i) + ((hash << 5) - hash);
  }
  return colors[Math.abs(hash) % colors.length];
}
 
export function Avatar({ name }: AvatarProps) {
  const initials = name
    .split(" ")
    .map((n) => n[0])
    .join("")
    .slice(0, 2)
    .toUpperCase();
 
  return (
    <div
      className={`flex h-10 w-10 items-center justify-center rounded-full text-sm font-medium text-white ${getColorFromName(name)}`}
    >
      {initials}
    </div>
  );
}

Assigns a consistent background color based on a hash of the user's name. The same name always produces the same color, making it easier to visually distinguish users in a list.

With Status Indicator

"use client";
 
type Status = "online" | "offline" | "busy" | "away";
 
interface AvatarProps {
  src?: string;
  name: string;
  status?: Status;
}
 
const statusColors: Record<Status, string> = {
  online: "bg-green-500",
  offline: "bg-gray-400",
  busy: "bg-red-500",
  away: "bg-yellow-500",
};
 
export function Avatar({ src, name, status }: AvatarProps) {
  const initials = name
    .split(" ")
    .map((n) => n[0])
    .join("")
    .slice(0, 2)
    .toUpperCase();
 
  return (
    <div className="relative inline-flex">
      {src ? (
        <img src={src} alt={name} className="h-10 w-10 rounded-full object-cover" />
      ) : (
        <div className="flex h-10 w-10 items-center justify-center rounded-full bg-blue-600 text-sm font-medium text-white">
          {initials}
        </div>
      )}
      {status && (
        <span
          className={`absolute bottom-0 right-0 h-3 w-3 rounded-full ring-2 ring-white ${statusColors[status]}`}
          aria-label={status}
        />
      )}
    </div>
  );
}

A colored dot positioned at the bottom-right corner indicates the user's status. The ring-2 ring-white creates a white border around the dot so it stands out against the avatar image.

Size Variants

"use client";
 
type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl";
 
interface AvatarProps {
  src?: string;
  name: string;
  size?: AvatarSize;
}
 
const sizeClasses: Record<AvatarSize, { container: string; text: string }> = {
  xs: { container: "h-6 w-6", text: "text-[10px]" },
  sm: { container: "h-8 w-8", text: "text-xs" },
  md: { container: "h-10 w-10", text: "text-sm" },
  lg: { container: "h-14 w-14", text: "text-lg" },
  xl: { container: "h-20 w-20", text: "text-2xl" },
};
 
export function Avatar({ src, name, size = "md" }: AvatarProps) {
  const sizes = sizeClasses[size];
  const initials = name
    .split(" ")
    .map((n) => n[0])
    .join("")
    .slice(0, 2)
    .toUpperCase();
 
  return src ? (
    <img src={src} alt={name} className={`rounded-full object-cover ${sizes.container}`} />
  ) : (
    <div
      className={`flex items-center justify-center rounded-full bg-blue-600 font-medium text-white ${sizes.container} ${sizes.text}`}
    >
      {initials}
    </div>
  );
}

Five size presets from xs (24px) to xl (80px). The font size scales proportionally with the container so initials remain readable at every size.

Avatar Group / Stack

"use client";
 
interface AvatarGroupProps {
  users: { name: string; src?: string }[];
  max?: number;
}
 
export function AvatarGroup({ users, max = 4 }: AvatarGroupProps) {
  const visible = users.slice(0, max);
  const remaining = users.length - max;
 
  return (
    <div className="flex -space-x-2">
      {visible.map((user) => {
        const initials = user.name
          .split(" ")
          .map((n) => n[0])
          .join("")
          .slice(0, 2)
          .toUpperCase();
 
        return user.src ? (
          <img
            key={user.name}
            src={user.src}
            alt={user.name}
            className="h-8 w-8 rounded-full border-2 border-white object-cover"
          />
        ) : (
          <div
            key={user.name}
            className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-white bg-blue-600 text-xs font-medium text-white"
          >
            {initials}
          </div>
        );
      })}
      {remaining > 0 && (
        <div className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-white bg-gray-200 text-xs font-medium text-gray-600">
          +{remaining}
        </div>
      )}
    </div>
  );
}

Uses -space-x-2 to overlap avatars horizontally. A border-2 border-white on each avatar creates visual separation. When the user count exceeds max, a "+N" badge shows the overflow count.

With Badge

"use client";
 
interface AvatarProps {
  src?: string;
  name: string;
  badge?: string | number;
}
 
export function Avatar({ src, name, badge }: AvatarProps) {
  const initials = name
    .split(" ")
    .map((n) => n[0])
    .join("")
    .slice(0, 2)
    .toUpperCase();
 
  return (
    <div className="relative inline-flex">
      {src ? (
        <img src={src} alt={name} className="h-10 w-10 rounded-full object-cover" />
      ) : (
        <div className="flex h-10 w-10 items-center justify-center rounded-full bg-blue-600 text-sm font-medium text-white">
          {initials}
        </div>
      )}
      {badge !== undefined && (
        <span className="absolute -right-1 -top-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-red-500 px-1 text-[10px] font-bold text-white ring-2 ring-white">
          {badge}
        </span>
      )}
    </div>
  );
}

A notification badge positioned at the top-right corner. The min-w-5 ensures the badge stays circular for single digits while expanding for larger numbers. The ring-2 ring-white separates the badge from the avatar visually.

Complex Implementation

"use client";
 
import { forwardRef, useState, useMemo } from "react";
import Image from "next/image";
 
type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl";
type Status = "online" | "offline" | "busy" | "away";
 
interface AvatarProps {
  src?: string;
  name: string;
  size?: AvatarSize;
  status?: Status;
  badge?: string | number;
  rounded?: "full" | "lg";
  className?: string;
}
 
const sizeConfig: Record<AvatarSize, { px: number; container: string; text: string; status: string; badge: string }> = {
  xs: { px: 24, container: "h-6 w-6", text: "text-[10px]", status: "h-2 w-2 ring-1", badge: "h-3.5 min-w-3.5 text-[8px] -right-0.5 -top-0.5" },
  sm: { px: 32, container: "h-8 w-8", text: "text-xs", status: "h-2.5 w-2.5 ring-2", badge: "h-4 min-w-4 text-[9px] -right-1 -top-1" },
  md: { px: 40, container: "h-10 w-10", text: "text-sm", status: "h-3 w-3 ring-2", badge: "h-5 min-w-5 text-[10px] -right-1 -top-1" },
  lg: { px: 56, container: "h-14 w-14", text: "text-lg", status: "h-3.5 w-3.5 ring-2", badge: "h-5 min-w-5 text-[10px] -right-0.5 -top-0.5" },
  xl: { px: 80, container: "h-20 w-20", text: "text-2xl", status: "h-4 w-4 ring-2", badge: "h-6 min-w-6 text-xs -right-1 -top-1" },
};
 
const statusColors: Record<Status, string> = {
  online: "bg-green-500",
  offline: "bg-gray-400",
  busy: "bg-red-500",
  away: "bg-yellow-500",
};
 
const bgColors = [
  "bg-red-600", "bg-orange-600", "bg-amber-600", "bg-green-600",
  "bg-teal-600", "bg-blue-600", "bg-indigo-600", "bg-purple-600", "bg-pink-600",
];
 
function hashName(name: string): number {
  let hash = 0;
  for (let i = 0; i < name.length; i++) {
    hash = name.charCodeAt(i) + ((hash << 5) - hash);
  }
  return Math.abs(hash);
}
 
export const Avatar = forwardRef<HTMLDivElement, AvatarProps>(function Avatar(
  { src, name, size = "md", status, badge, rounded = "full", className },
  ref
) {
  const [imgFailed, setImgFailed] = useState(false);
  const config = sizeConfig[size];
  const roundedClass = rounded === "full" ? "rounded-full" : "rounded-lg";
 
  const initials = useMemo(
    () =>
      name
        .split(" ")
        .map((n) => n[0])
        .join("")
        .slice(0, 2)
        .toUpperCase(),
    [name]
  );
 
  const bgColor = useMemo(() => bgColors[hashName(name) % bgColors.length], [name]);
 
  return (
    <div ref={ref} className={`relative inline-flex shrink-0 ${className ?? ""}`}>
      {src && !imgFailed ? (
        <Image
          src={src}
          alt={name}
          width={config.px}
          height={config.px}
          onError={() => setImgFailed(true)}
          className={`${config.container} ${roundedClass} object-cover`}
        />
      ) : (
        <div
          className={`flex items-center justify-center font-medium text-white ${config.container} ${config.text} ${roundedClass} ${bgColor}`}
          role="img"
          aria-label={name}
        >
          {initials}
        </div>
      )}
 
      {status && (
        <span
          className={`absolute bottom-0 right-0 ${roundedClass === "rounded-full" ? "rounded-full" : "rounded-full"} ring-white ${config.status} ${statusColors[status]}`}
          aria-label={`Status: ${status}`}
        />
      )}
 
      {badge !== undefined && (
        <span
          className={`absolute flex items-center justify-center rounded-full bg-red-500 px-0.5 font-bold text-white ring-2 ring-white ${config.badge}`}
        >
          {typeof badge === "number" && badge > 99 ? "99+" : badge}
        </span>
      )}
    </div>
  );
});
 
// --- Avatar Group ---
 
interface AvatarGroupProps {
  users: { name: string; src?: string }[];
  max?: number;
  size?: AvatarSize;
}
 
export function AvatarGroup({ users, max = 4, size = "sm" }: AvatarGroupProps) {
  const visible = users.slice(0, max);
  const remaining = users.length - max;
  const config = sizeConfig[size];
 
  return (
    <div className="flex -space-x-2" role="group" aria-label={`${users.length} users`}>
      {visible.map((user) => (
        <Avatar
          key={user.name}
          src={user.src}
          name={user.name}
          size={size}
          className="border-2 border-white"
        />
      ))}
      {remaining > 0 && (
        <div
          className={`flex items-center justify-center rounded-full border-2 border-white bg-gray-200 font-medium text-gray-600 ${config.container} ${config.text}`}
        >
          +{remaining}
        </div>
      )}
    </div>
  );
}

Key aspects:

  • Next.js Image optimization -- uses next/image for automatic resizing, lazy loading, and format conversion. Falls back to initials on error.
  • Deterministic color hashing -- the same user name always produces the same background color, providing visual consistency across the application without storing color preferences.
  • Scaled indicators -- status dots and badge sizes are proportional to the avatar size, so everything looks correct at xs through xl.
  • forwardRef -- allows parent components to measure, position, or animate the avatar with refs.
  • Badge overflow -- numbers above 99 are capped to "99+" to prevent the badge from growing too wide and breaking the layout.
  • role="img" on fallback -- the initials div is marked as an image role with aria-label so screen readers announce the user's full name rather than reading individual letters.
  • shrink-0 -- prevents the avatar from shrinking inside flex containers, which is a common layout issue in sidebars and user lists.

Gotchas

  • Missing alt on images -- Omitting the alt attribute makes the avatar invisible to screen readers. Always set alt to the user's name.

  • Image aspect ratio distortion -- Non-square profile images stretch without object-cover. Always use object-cover on rounded avatar images.

  • Initials from empty strings -- "".split(" ") returns [""], and ""[0] is undefined. Guard against empty or undefined names before extracting initials.

  • Status dot invisible on dark backgrounds -- A green "online" dot disappears against a green avatar. The ring-white border is essential for contrast in all color combinations.

  • Layout shift from image loading -- Without explicit width and height, the avatar collapses to 0px until the image loads. Always set fixed dimensions on the container.

  • Avatar group z-index stacking -- Overlapping avatars may render in the wrong order. Add hover:z-10 if you want hovered avatars to pop above their neighbors.

  • Badge -- Notification badges used with avatars
  • Skeleton -- Loading placeholders for avatar images
  • Button -- Avatar used inside button-like clickable elements