Skip to content

Avatar

SM · 950B gzip budget

A person's picture, or script-aware initials when there is none.

Category
Data display
Budget
sm, single-purpose control
External runtime
React / React DOM only
axe fixtureSSR rendersource scanbundle budget

Preview

Live fixture
Preview locale

Try the component here. Locale-aware formatting and direction follow the selected language; example content and labels are not automatically translated. Overlay demos use interactive launchers.

Install

npx shadcn@latest add https://gear5-ui.vercel.app/r/avatar.json

Copies the source into your project. Pulls in 1 shared registry item: cn.

New here? Set up Tailwind and import aliases first →

Source & props

Edit on GitHub ↗

Prop types and inline documentation are included below. This is repository source; the installer rewrites shared imports for your project.

"use client";

import { useState } from "react";
import { cn } from "../lib/cn";

export interface AvatarProps {
  src?: string;
  /** Person's name. Used for the accessible label and the initials fallback. */
  name: string;
  size?: number;
  className?: string;
}

/**
 * `Intl.Segmenter` splits on grapheme clusters, not UTF-16 code units, so the
 * first "letter" of a name is correct for combining scripts (Devanagari,
 * Thai) and for names in scripts with no concept of upper/lower case (CJK,
 * Arabic) alike — a naive `name[0].toUpperCase()` mangles all three.
 */
function initials(name: string): string {
  const trimmed = name.trim();
  if (!trimmed) return "";

  const words = trimmed.split(/\s+/).slice(0, 2);
  const segmenter = typeof Intl.Segmenter === "function" ? new Intl.Segmenter() : null;

  return words
    .map((word) => {
      if (!segmenter) return word[0] ?? "";
      const first = segmenter.segment(word)[Symbol.iterator]().next();
      return first.done ? "" : first.value.segment;
    })
    .join("")
    .toUpperCase();
}

/** A person's picture, or their initials when there is none or it fails to load. */
export function Avatar({ src, name, size = 40, className }: AvatarProps) {
  const [failed, setFailed] = useState(false);
  const showImage = src && !failed;

  return (
    <span
      role="img"
      aria-label={name}
      className={cn(
        "inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-neutral-200 font-medium text-neutral-700 dark:bg-neutral-700 dark:text-neutral-200",
        className,
      )}
      style={{ width: size, height: size, fontSize: size * 0.4 }}
    >
      {showImage ? (
        // eslint-disable-next-line @next/next/no-img-element -- framework-agnostic by design; see adaptive-image.tsx for the fuller rationale.
        <img
          src={src}
          alt=""
          aria-hidden="true"
          width={size}
          height={size}
          className="size-full object-cover"
          onError={() => setFailed(true)}
        />
      ) : (
        <span aria-hidden="true">{initials(name)}</span>
      )}
    </span>
  );
}

What CI checks

Quality contract
  • Bundled, minified and gzipped against its tier budget
  • Audited by axe in the state previewed above
  • Rendered through react-dom/server with no browser globals
  • Scanned for network calls, dangerous sinks, and unguarded animation

Used by