Avatar
Muestra la foto de perfil de un usuario con una alternativa de iniciales cuando no hay imagen disponible, comúnmente se utiliza en barras de navegación, hilos de comentarios y listas de usuarios.
Busca en todas las páginas de la documentación
Muestra la foto de perfil de un usuario con una alternativa de iniciales cuando no hay imagen disponible, comúnmente se utiliza en barras de navegación, hilos de comentarios y listas de usuarios.
🤖 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>
);
}Renderiza una imagen cuando se proporciona src, de lo contrario se revierte a iniciales extraídas del nombre del usuario. La clase object-cover evita la distorsión de imagen en fotos no cuadradas.
"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"
/>
);
}Maneja elegantemente URLs de imagen rotos capturando el evento onError y revirtiendo a iniciales. Sin esto, una imagen rota muestra el icono de imagen rota predeterminado del navegador.
"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>
);
}Asigna un color de fondo consistente basado en un hash del nombre del usuario. El mismo nombre siempre produce el mismo color, lo que facilita distinguir visualmente a los usuarios en una lista.
"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>
);
}Un punto de color posicionado en la esquina inferior derecha indica el estado del usuario. ring-2 ring-white crea un borde blanco alrededor del punto para que destaque contra la imagen del avatar.
"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>
);
}Cinco presets de tamaño de xs (24px) a xl (80px). El tamaño de fuente se escala proporcionalmente con el contenedor para que las iniciales permanezcan legibles en todos los tamaños.
"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>
);
}Usa -space-x-2 para superponer avatares horizontalmente. Un border-2 border-white en cada avatar crea separación visual. Cuando el número de usuarios excede max, una insignia "+N" muestra el contador de desbordamiento.
"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>
);
}Una insignia de notificación posicionada en la esquina superior derecha. min-w-5 asegura que la insignia permanezca circular para números de un dígito mientras se expande para números más grandes. ring-2 ring-white separa la insignia del avatar visualmente.
"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>
);
}Aspectos clave:
next/image para cambio de tamaño automático, carga perezosa y conversión de formato. Se revierte a iniciales en error.xs a xl.aria-label para que los lectores de pantalla anuncien el nombre completo del usuario en lugar de leer letras individuales.Falta de alt en imágenes -- Omitir el atributo alt hace que el avatar sea invisible para los lectores de pantalla. Siempre establece alt al nombre del usuario.
Distorsión de relación de aspecto de imagen -- Las imágenes de perfil no cuadradas se estiran sin object-cover. Siempre usa object-cover en imágenes de avatar redondeadas.
Iniciales de cadenas vacías -- "".split(" ") devuelve [""], y ""[0] es undefined. Protege contra nombres vacíos o indefinidos antes de extraer iniciales.
Punto de estado invisible en fondos oscuros -- Un punto "en línea" verde desaparece contra un avatar verde. El borde ring-white es esencial para el contraste en todas las combinaciones de color.
Cambio de diseño por carga de imagen -- Sin ancho y alto explícitos, el avatar se colapsa a 0px hasta que se carga la imagen. Siempre establece dimensiones fijas en el contenedor.
Z-index de apilamiento de grupo de avatares -- Los avatares superpuestos pueden renderizarse en el orden incorrecto. Agrega hover:z-10 si quieres que los avatares desplazados sobresalgan por encima de sus vecinos.
Revisado por Chris St. John·Última actualización: 7 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥