Ir al contenido
Evaluar mi proyecto
Componentes · Componentes baseDisponible

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.

base-avatares.tsx69 líneas
Descargar
"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.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/base-avatares

Ejemplo 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.

ejemplo.tsx5 líneas
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.

PropiedadTipoDescripción
people?BaseAvataresPersona[]Opcional.
max?numberOpcional.
size?numberOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

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".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)