Avatar
Un círculo para personas y un cuadrado más pequeño para espacios de trabajo, con iniciales centradas por altura de mayúsculas.

Instalación
$ pnpm dlx shadcn@latest add @ssntl/avatarUso
import { Avatar } from "@/components/ui/avatar"
<Avatar name="Olivia Sosa" />
<Avatar shape="square" name="Studio Norte" />
<Avatar name="Olivia Sosa" src="/olivia.webp" />Reglas
Lo que este componente espera de ti, tal como se instala en components/ui/avatar.md junto al código. El archivo completo agrega cuándo usarlo, teclado, los fundamentos de UI que aplica y cada regla compartida (en inglés).
## Reglas de Avatar
- Área de íconos: círculos para personas, cuadrados para espacios de trabajo. El cuadrado se dibuja cerca de 0.89× el círculo (48 → 44), así ambos tienen la misma área y se ven del mismo tamaño.
- Las iniciales se centran por la altura de mayúsculas (`text-trim`), así quedan en el centro óptico.
- **Pasa name.** Nombra el avatar para lectores de pantalla y da las iniciales, contadas por grafemas para que emoji, acentos y CJK queden enteros (🦊 Fox → 🦊F, 王秀英 → 王).
- Si src no carga, se muestran las iniciales en lugar de una imagen rota.
- Sin name, pasa tú `role="img"` y un `aria-label`; un avatar junto a un nombre visible puede quedar decorativo.
- Un contorno de 1px al 10% mantiene el borde de una foto sobre cualquier superficie.Código
"use client"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
// A person (circle) or a workspace (square). Icon area: a square of the same
// width as a circle holds 27% more area and looks bigger, so squares are
// drawn about 0.89× the circle (equal area): 48 → 44, 36 → 32, 56 → 50.
// Initials are centered by their cap height (text-trim), not the line box,
// so "OS" sits in the optical middle. A broken image falls back to the
// initials, so a 404 never shows as a broken-image icon.
const avatarVariants = cva(
"inline-grid shrink-0 place-items-center overflow-hidden bg-secondary font-medium text-foreground uppercase outline -outline-offset-1 outline-black/10 dark:outline-white/10 [&_img]:size-full [&_img]:object-cover",
{
variants: {
shape: { circle: "rounded-full", square: "rounded-xs" },
size: { sm: "text-caption", md: "text-label", lg: "text-body" },
},
compoundVariants: [
{ shape: "circle", size: "sm", className: "size-9" },
{ shape: "circle", size: "md", className: "size-12" },
{ shape: "circle", size: "lg", className: "size-14" },
{ shape: "square", size: "sm", className: "size-8" },
{ shape: "square", size: "md", className: "size-11" },
{ shape: "square", size: "lg", className: "size-12.5" },
],
defaultVariants: { shape: "circle", size: "md" },
}
)
/**
* Initials from a full name, counted in graphemes so emoji and accents stay
* whole: "Ana López" → "AL", "🦊 Fox" → "🦊F", "王秀英" → "王", "dana" → "D".
*/
function getInitials(name: string, locale?: string) {
const words = name.trim().split(/\s+/).filter(Boolean)
if (words.length === 0) return ""
const graphemes = new Intl.Segmenter(locale, { granularity: "grapheme" })
const first = (word: string) =>
graphemes.segment(word)[Symbol.iterator]().next().value?.segment ?? ""
const picks = words.length === 1 ? [words[0]!] : [words[0]!, words.at(-1)!]
return picks.map(first).join("").toLocaleUpperCase(locale)
}
function Avatar({
shape,
size,
name,
src,
className,
children,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof avatarVariants> & {
/** The person's or workspace's name: names the avatar and gives initials. */
name?: string
/** A photo or logo; if it fails to load, the initials show instead. */
src?: string
}) {
const [failed, setFailed] = React.useState<string | null>(null)
const fallback = children ?? (name ? getInitials(name) : null)
const showImage = src && failed !== src
return (
<span
data-slot="avatar"
role={name ? "img" : undefined}
aria-label={name}
className={cn(avatarVariants({ shape, size, className }))}
{...props}
>
{showImage ? (
// eslint-disable-next-line @next/next/no-img-element -- any image source
<img src={src} alt="" onError={() => setFailed(src)} />
) : typeof fallback === "string" ? (
<span aria-hidden={name ? true : undefined} className="text-trim">
{fallback}
</span>
) : (
fallback
)}
</span>
)
}
export { Avatar, avatarVariants, getInitials }