Pho Design System

Avatar

Represents a user or entity with an image, falling back to initials or an icon when the image is missing or still loading. Built on Base UI Avatar, which manages the load/error state for you.

Import

import { Avatar } from "@photon-ai/pho-ui/components/avatar";

Sizes

Three sizes — sm, md, lg. The default is md. Set size on Avatar.Root.

GHGHGH
<Avatar.Root size="sm">
  <Avatar.Image src="/grace.jpg" alt="Grace Hopper" />
  <Avatar.Fallback>GH</Avatar.Fallback>
</Avatar.Root>

Fallback

Avatar.Fallback renders while the image loads and stays if it fails or no Avatar.Image is given. Use initials or an icon; tint it for variety.

PBALLT
<Avatar.Root>
  <Avatar.Fallback>PB</Avatar.Fallback>
</Avatar.Root>

<Avatar.Root className="bg-pho-brand-secondary text-pho-brand">
  <Avatar.Fallback>AL</Avatar.Fallback>
</Avatar.Root>

Edit

Avatar.Edit is a full-face button overlay on bg-pho-overlay, revealed on hover / focus-visible (and while a Menu opened from it is open). Swap the icon with children. Return a promise from onClick to keep the overlay visible with a spinner until it settles. There is no built-in file input.

Pencil (default) — open a local file picker, then simulate upload and replace the photo. Loading starts only after a file is chosen — not while the native dialog is open.

GH

Hover → pencil → choose an image. Upload spinner only after you pick.

function changeFromFile() {
  void (async () => {
    const file = await pickImageFile(); // no loading yet
    if (!file) return;
    setUploading(true); // spinner only after pick
    try {
      setSrc(await uploadLocalAvatar(file));
    } finally {
      setUploading(false);
    }
  })();
}

<Avatar.Root size="lg">
  {src ? <Avatar.Image src={src} alt="Grace Hopper" /> : null}
  <Avatar.Fallback>GH</Avatar.Fallback>
  <Avatar.Edit
    label="Change photo"
    loading={uploading}
    onClick={changeFromFile}
  />
</Avatar.Root>;

Dots + Menu — when a custom photo is set, pass IconDots and open a menu (Change / Remove). Change uses the same local picker; Remove clears after a short delay. When the avatar is already the default fallback, drop the menu and use the pencil for a single change action.

GH

Dots — change (file picker) or remove.

import { IconDots, IconPencil, IconTrash } from "@tabler/icons-react";
import { Menu } from "@photon-ai/pho-ui/components/menu";

<Avatar.Root size="lg">
  {src ? <Avatar.Image src={src} alt="Grace Hopper" /> : null}
  <Avatar.Fallback>GH</Avatar.Fallback>
  {src ? (
    <Menu.Root>
      <Menu.Trigger
        render={
          <Avatar.Edit label="Photo options">
            <IconDots />
          </Avatar.Edit>
        }
      />
      <Menu.Portal>
        <Menu.Positioner align="start">
          <Menu.Popup>
            <Menu.Item onClick={changeFromFile}>
              <IconPencil />
              Change
            </Menu.Item>
            <Menu.Item onClick={remove}>
              <IconTrash />
              Remove
            </Menu.Item>
          </Menu.Popup>
        </Menu.Positioner>
      </Menu.Portal>
    </Menu.Root>
  ) : (
    <Avatar.Edit label="Change photo" onClick={changeFromFile} />
  )}
</Avatar.Root>;

Anatomy

<Avatar.Root size="md">
  <Avatar.Image src="…" alt="…" />
  <Avatar.Fallback>…</Avatar.Fallback>
  <Avatar.Edit label="Change photo" onClick={…} />
  {/* or Menu.Trigger render={<Avatar.Edit><IconDots /></Avatar.Edit>} */}
</Avatar.Root>

Props

Avatar.Root

Avatar.Image

Avatar.Fallback

Avatar.Edit

Accessibility

Avatar.Root renders a <span>, Avatar.Image an <img>, and Avatar.Fallback a <span>. The avatar’s accessible name comes from the image’s alt; when the same name is already shown beside it, pass alt="" so it isn’t announced twice.

Base UI tracks the image’s loading status: the <img> stays hidden until it loads, and Avatar.Fallback is shown until then or if the load fails. Initials in the fallback are plain text, so they still convey identity when no photo loads.

Without Avatar.Edit, the avatar isn’t interactive. When the avatar itself is the control, compose Avatar.Edit — a real <button> with an aria-label and focus ring. Use the pencil for one action; use IconDots + Menu when the user can change or remove. For navigating to a profile, wrap the avatar in a link instead.

Best practices