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.
Search across all documentation pages
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.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
"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.
"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.
"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.
"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.
"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.
"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.
"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.
"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/image for automatic resizing, lazy loading, and format conversion. Falls back to initials on error.xs through xl.aria-label so screen readers announce the user's full name rather than reading individual letters.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.
Reviewed by Chris St. John·Last updated Jul 7, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥