Skip to content
Primitiv home
Framework
Consumption mode

Avatar

stableSource Figma

A user image with a graceful fallback for while it loads, or when it is missing or broken.

Playground

Density

Preview

Ada LovelaceAL
Size
Shape
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
<Avatar size="md" shape="circle">  <AvatarImage src="/avatar-demo.jpg" alt="Ada Lovelace" />  <AvatarFallback>AL</AvatarFallback></Avatar>

Density is set by a data-density ancestor — the Context system, not a Avatar prop.

Installation

npx primitiv add avatar

Import

import { Avatar } from "@/components/ui/avatar";

Copied into your project as .primitiv-avatar — you own the file afterwards, so upgrades are opt-in.

Headless mode installs the npm package instead: @primitiv-ui/react

Anatomy

<Avatar>  <AvatarImage src="/avatar-demo.jpg" alt="Ada Lovelace" />  <AvatarFallback>AL</AvatarFallback></Avatar>

Props

Avatar.Root

Extends HTMLSpanElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRender the consumer's own element instead of the native <span>, merging the data-status hook onto it via Slot.
size"xs" | "sm" | "md" | "lg" | "xl"mdstyledControl size; data-density scales each size further.
shape"circle" | "square"circlestyledCorner treatment — fully rounded, or square with size-scaled corners.

Avatar.Image

Extends HTMLImageElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRender the consumer's own <img> instead of the native one, merging the load handlers, ref, and data-status hook onto it via Slot.

Avatar.Fallback

Extends HTMLSpanElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRender the consumer's own element instead of the native <span>, merging the data-status hook onto it via Slot.
delayMsnumber—headlessWithhold the fallback for this many milliseconds after mount. Useful to avoid a flash of fallback content on fast connections where the image loads almost immediately. Omit to render the fallback straight away.

Styling contract

--primitiv-avatar-size--primitiv-avatar-icon-size--primitiv-avatar-radius--primitiv-avatar-square-radius--primitiv-avatar-border-width--primitiv-avatar-border-color--primitiv-avatar-fallback-bg--primitiv-avatar-fallback-fg--primitiv-avatar-fallback-font-family--primitiv-avatar-fallback-font-size--primitiv-avatar-fallback-font-weight

Data attributes

Avatar

className: .primitiv-avatar

AttributeValueWhen
data-statusidle | loading | loaded | erroridle / loading / loaded / error

AvatarImage

className: .primitiv-avatar__image

AttributeValueWhen
data-statusidle | loading | loaded | erroridle / loading / loaded / error

AvatarFallback

className: .primitiv-avatar__fallback

AttributeValueWhen
data-statusidle | loading | loaded | erroridle / loading / loaded / error

Accessibility

  • Give the image a considered alt. If the avatar sits next to the person's name, the picture is decorative — pass alt="" so a screen reader does not announce the name twice. If it stands alone (a bare avatar in a toolbar), alt should name the person, since it is the only label they get.
  • A fallback needs a name too. Initials render as text and are announced as-is — which is often not much use ("AL"). An icon fallback should be aria-hidden (as the example shows) so it announces nothing, with the name coming from an aria-label on Avatar.Root or from adjacent text. Decide what the avatar should announce and put the name in one place.
  • data-status (idle / loading / loaded / error) is a styling hook, not a live region — the component announces no loading state to assistive technology, which is correct: a picture quietly resolving is not news worth interrupting a screen-reader user for.
  • size and shape are purely visual and change nothing about the accessibility tree — a large square avatar and a small circular one read identically. Do not lean on size to convey meaning.
  • By default the Root is a non-interactive <span>. If you make the avatar a link or button with asChild, it becomes a focus target, so it then needs an accessible name and a visible :focus-visible ring like any other control — the avatar image alone is not a label.
  • The default fallback pairs action/secondary background and foreground, which meets contrast out of the box. If you recolour it through the --primitiv-avatar-fallback-bg / -fg custom properties, keep the pair legible — initials at small sizes are already a demanding contrast case.

Examples

Image with a fallback (the headline)

The normal shape: an Avatar.Image for the photo and an Avatar.Fallback for while it loads, or if it is missing. Avatar.Root holds a single data-status (idle → loading → loaded / error); the Image reports its transitions up and the Fallback shows through until the image is loaded. Both fill the same clipping frame and only one is visible, so there is no crossfade — and the <img> stays mounted through every status, so its load lifecycle is never lost.

Density
Ada LovelaceAL
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
<Avatar>  <AvatarImage src="/avatar-demo.jpg" alt="Ada Lovelace" />  <AvatarFallback>AL</AvatarFallback></Avatar>

Fallback: initials or an icon

With no Avatar.Image — or before one loads — the Fallback is what shows. Initials are the usual choice when you have a name; a neutral icon like User covers the case where you do not. The styled Fallback wraps text children in a label span so text-box-trim can centre the glyphs optically, but passes an element child (the icon) straight through, so an icon sits centred by the flex frame instead. Mind the accessible name either way — see the notes below.

Density
AL
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";import { User } from "@primitiv-ui/icons";import { Stack } from "@/components/ui/stack";
<Stack direction="row" gap="sm">  <Avatar>    <AvatarFallback>AL</AvatarFallback>  </Avatar>  <Avatar aria-label="Ada Lovelace">    <AvatarFallback><User aria-hidden="true" /></AvatarFallback>  </Avatar></Stack>

When the image is missing or broken

A src that 404s (or none at all) never shows a broken-image glyph: the <img> is hidden rather than unmounted and the Fallback shows through, so a missing photo degrades to initials. On fast connections a fallback can instead flash before a working image decodes; delayMs on the Fallback withholds it for that many milliseconds after mount to avoid the flicker. It is a Fallback prop, listed in the props table below.

Density
Grace HopperGH
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
<Avatar>  <AvatarImage src="/does-not-exist.jpg" alt="Grace Hopper" />  <AvatarFallback>GH</AvatarFallback></Avatar>

Shape

shape is "circle" by default — fully rounded at any size — or "square", which swaps in size-scaled corners rather than a hard right angle, so a small square avatar and a large one keep the same visual softness. The corner radius is its own avatar/radius/* token family, independent of the shared sizing scale.

Density
Ada LovelaceALAda LovelaceAL
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";import { Stack } from "@/components/ui/stack";
<Stack direction="row" gap="sm">  <Avatar>    <AvatarImage src="/avatar-demo.jpg" alt="Ada Lovelace" />    <AvatarFallback>AL</AvatarFallback>  </Avatar>  <Avatar shape="square">    <AvatarImage src="/avatar-demo.jpg" alt="Ada Lovelace" />    <AvatarFallback>AL</AvatarFallback>  </Avatar></Stack>

Sizes and density

Five sizes, each rescaling again with the nearest data-density ancestor. Sizing reuses the shared framed-control/* scale directly — the same token an Input or Button of that size uses — so an avatar lines up cleanly beside a control of the matching size rather than needing its own scale.

Density
Ada LovelaceALAda LovelaceALAda LovelaceALAda LovelaceALAda LovelaceAL
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";import { Stack } from "@/components/ui/stack";
<div data-density="comfortable">  <Stack direction="row" gap="sm">    <Avatar size="xs">      <AvatarImage src="/avatar-demo.jpg" alt="Ada Lovelace" />      <AvatarFallback>AL</AvatarFallback>    </Avatar>    <Avatar size="sm">      <AvatarImage src="/avatar-demo.jpg" alt="Ada Lovelace" />      <AvatarFallback>AL</AvatarFallback>    </Avatar>    <Avatar size="md">      <AvatarImage src="/avatar-demo.jpg" alt="Ada Lovelace" />      <AvatarFallback>AL</AvatarFallback>    </Avatar>    <Avatar size="lg">      <AvatarImage src="/avatar-demo.jpg" alt="Ada Lovelace" />      <AvatarFallback>AL</AvatarFallback>    </Avatar>    <Avatar size="xl">      <AvatarImage src="/avatar-demo.jpg" alt="Ada Lovelace" />      <AvatarFallback>AL</AvatarFallback>    </Avatar>  </Stack></div>