Grupo de avatares con estado
Avatares con iniciales, anillo de estado y contador +N que se abren en abanico al apuntarlos.
- AP
- LG
- MR
- DS
- +3
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import type { CSSProperties } from "react";
/** Grupo de avatares superpuestos con iniciales, anillo de estado y contador "+N"; se abren en abanico al apuntarlos. Sin imágenes externas. React + TypeScript, sin dependencias. */
export type BaseAvataresPersona = { name: string; status?: "en-linea" | "ocupado" | "ausente" };
export type BaseAvataresProps = {
people?: BaseAvataresPersona[];
max?: number;
size?: number;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcbav-root { box-sizing: border-box; display: inline-flex; align-items: center; margin: 0; padding: 0; list-style: none; font: 700 13px/1 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
.gcbav-root *, .gcbav-root *::before, .gcbav-root *::after { box-sizing: border-box; }
@media (prefers-reduced-motion: reduce) { .gcbav-root * { transition-duration: .01ms !important; } }
.gcbav-item { position: relative; margin-left: calc(var(--z) * -.3); transition: margin .35s cubic-bezier(.16, 1, .3, 1); }
.gcbav-item:first-child { margin-left: 0; }
.gcbav-root:hover .gcbav-item, .gcbav-root:focus-within .gcbav-item { margin-left: 6px; }
.gcbav-root:hover .gcbav-item:first-child, .gcbav-root:focus-within .gcbav-item:first-child { margin-left: 0; }
.gcbav-av { position: relative; display: grid; place-items: center; width: var(--z); height: var(--z); border-radius: 50%; background: var(--c); color: #fff; border: 3px solid #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, .15); cursor: default; transition: transform .25s cubic-bezier(.34, 1.56, .64, 1); text-shadow: 0 1px 1px rgba(0, 0, 0, .2); }
.gcbav-av:hover, .gcbav-av:focus-visible { transform: translateY(-4px) scale(1.08); z-index: 2; outline: 0; }
.gcbav-av:focus-visible { box-shadow: 0 0 0 3px #2563eb; }
.gcbav-st { position: absolute; right: -1px; bottom: -1px; width: 12px; height: 12px; border-radius: 50%; border: 2px solid #fff; background: var(--st); }
.gcbav-more { background: #eceef3; color: #3f4658; text-shadow: none; }
.gcbav-tip { position: absolute; bottom: calc(100% + 8px); left: 50%; padding: 5px 8px; border-radius: 6px; background: #18181b; color: #fff; font-size: 12px; white-space: nowrap; opacity: 0; pointer-events: none; transform: translate(-50%, 4px); transition: opacity .15s, transform .15s; }
.gcbav-av:hover + .gcbav-tip, .gcbav-av:focus-visible + .gcbav-tip { opacity: 1; transform: translate(-50%, 0); }`;
const COLORS = ["#2f5bff", "#ff7a45", "#18b37e", "#9b5de5", "#e5489a", "#0ea5b7"];
const STATUS = { "en-linea": "#22c55e", ocupado: "#ef4444", ausente: "#f59e0b" };
const initials = (name: string) => name.split(" ").map((part) => part[0]).slice(0, 2).join("").toUpperCase();
const DEFAULT: BaseAvataresPersona[] = [
{ name: "Ana Pérez", status: "en-linea" },
{ name: "Luis Gómez", status: "ocupado" },
{ name: "Marta Ruiz", status: "en-linea" },
{ name: "Diego Soto", status: "ausente" },
{ name: "Paula Díaz" },
{ name: "Raúl Vega" },
{ name: "Inés Mora" },
];
export function BaseAvatares({ people = DEFAULT, max = 4, size = 48, className = "", style }: BaseAvataresProps) {
const shown = people.slice(0, max);
const rest = people.length - shown.length;
return (
<ul className={`gcbav-root ${className}`} style={{ "--z": `${size}px`, ...style } as CSSProperties} aria-label={`${people.length} personas`}>
<style>{CSS}</style>
{shown.map((person, index) => (
<li key={person.name} className="gcbav-item" style={{ zIndex: shown.length - index }}>
<span className="gcbav-av" tabIndex={0} role="img" aria-label={`${person.name}${person.status ? `, ${person.status.replace("-", " ")}` : ""}`} style={{ "--c": COLORS[index % COLORS.length] } as CSSProperties}>
{initials(person.name)}
{person.status ? <i className="gcbav-st" style={{ "--st": STATUS[person.status] } as CSSProperties} /> : null}
</span>
<span className="gcbav-tip" aria-hidden="true">{person.name}</span>
</li>
))}
{rest > 0 ? (
<li className="gcbav-item" style={{ zIndex: 0 }}>
<span className="gcbav-av gcbav-more" role="img" aria-label={`${rest} personas más`}>+{rest}</span>
</li>
) : null}
</ul>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/base-avataresEjemplo de uso de Grupo de avatares con estado
Después de copiar base-avatares.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { BaseAvatares } from "./base-avatares";
export default function Ejemplo() {
return <BaseAvatares />;
}Propiedades de BaseAvatares
BaseAvatares acepta 5 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
people? | BaseAvataresPersona[] | Opcional. |
max? | number | Opcional. |
size? | number | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 69 líneas (4 KB) y no necesita @keyframes. Esto es lo que hace Grupo de avatares con estado por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
- Declara roles ARIA para lectores de pantalla: img.
- Es un componente de cliente de Next.js: incluye la directiva "use client".