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.
<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.
<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.
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.
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>
- Avatar.Root — circular container; takes
size(xs·sm·md·lg) - Avatar.Image — the photo; covers the container, hidden until loaded
- Avatar.Fallback — initials or icon shown until/unless the image loads
- Avatar.Edit — optional overlay; pencil by default, customize via
children
Props
Avatar.Root
- size —
xs·sm·md·lg(defaultmd) - className —
string— extend the container, e.g. to tint the fallback
Avatar.Image
- src —
string— the image URL - alt —
string— the accessible name for the image - onLoadingStatusChange —
(status) => void; status isidle·loading·loaded·error
Avatar.Fallback
- delay —
number— ms to wait before showing the fallback, to avoid a flash before a fast image loads
Avatar.Edit
- label —
string— accessible name (defaultEdit) - onClick —
(event) => void | Promise<unknown>— return a promise to keep the overlay open with a spinner until it settles; resolve clears loading, reject stays interactive. Skip when used as a Menu trigger - loading —
boolean(defaultfalse) — declarative loading (same spinner as an in-flight promise) - children — replace the default pencil; use
IconDotsfor a Menu of actions - Standard button attrs (
disabled,className, …); forwards ref forMenu.Trigger
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
- Always give
Avatar.Imagea meaningfulalt, and always provide aAvatar.Fallback— networks fail and not everyone has a photo. - Prefer initials over a generic silhouette; they identify the person faster.
- Keep avatars square-sourced. The container crops to a circle, so off-center or non-square images get clipped.
- Pass a specific
labelonAvatar.Edit("Change photo"or"Photo options") so the control isn’t announced as a bare “Edit”. - Pencil when the avatar is default (only change);
IconDots+ Menu when a custom photo can also be removed.