Avatar
A circle for people, a smaller square for workspaces, with initials centered by cap height.

Installation
$ pnpm dlx shadcn@latest add @ssntl/avatarUsage
import { Avatar } from "@/components/ui/avatar"
<Avatar name="Olivia Sosa" />
<Avatar shape="square" name="Studio Norte" />
<Avatar name="Olivia Sosa" src="/olivia.webp" />Rules
What this component expects from you, as it ships in components/ui/avatar.md next to the code. The full file adds when to use it, keyboard, the UI foundations it applies and every shared rule.
## Rules for Avatar
- **Icon area.** Circles for people, squares for workspaces. A square is drawn about 0.89Γ the circle (48 β 44) so both hold the same area and look the same size.
- Initials are centered by cap height (`text-trim`), so they sit in the optical middle.
- **Pass name.** It names the avatar for screen readers and gives the initials, counted in graphemes so emoji, accents and CJK stay whole (π¦ Fox β π¦F, ηη§θ± β η).
- If src fails to load, the initials show instead of a broken image.
- Without name, pass `role="img"` and an `aria-label` yourself; an avatar beside a visible name can stay decorative.
- A 1px outline at 10% keeps a photo's edge on any surface.Code
"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 }