Density is set by a data-density ancestor — the Context system, not a Avatar prop.
Installation
npx primitiv add avatar
pnpm dlx primitiv add avatar
yarn dlx primitiv add avatar
bunx 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
Three parts, and both surfaces carry all three — the styled Avatar / AvatarImage / AvatarFallback are thin wrappers over the headless Avatar.Root / .Image / .Fallback, so only the names differ between modes. Avatar.Root is a fixed-size clipping frame that owns the single data-status; Avatar.Image and Avatar.Fallback both fill it, and only one is ever visible. The Image is optional — a fallback-only avatar is valid — but the Root and a Fallback are not: without a Fallback there is nothing to show while the image is idle, loading or broken.
Generated from source — headless rows from each *Props type’s JSDoc, styled rows from contract.json. Never hand-maintained.
Avatar.Root
Extends HTMLSpanElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Render 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"
md
styled
Control size; data-density scales each size further.
shape
"circle" | "square"
circle
styled
Corner treatment — fully rounded, or square with size-scaled corners.
Avatar.Image
Extends HTMLImageElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Render 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.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Render the consumer's own element instead of the native <span>, merging
the data-status hook onto it via Slot.
delayMs
number
—
headless
Withhold 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
11 CSS custom properties on .primitiv-avatar — mode-agnostic. These names are the stable surface; the values are not.
Emitted automatically by the headless primitive — style against these rather than adding your own state classes. Grouped by the part that emits them, since most are emitted by more than one.
Avatar
className:.primitiv-avatar
Attribute
Value
When
data-status
idle | loading | loaded | error
idle / loading / loaded / error
Avatar.Root
Attribute
Value
When
data-status
idle | loading | loaded | error
idle / loading / loaded / error
AvatarImage
className:.primitiv-avatar__image
Attribute
Value
When
data-status
idle | loading | loaded | error
idle / loading / loaded / error
Avatar.Image
Attribute
Value
When
data-status
idle | loading | loaded | error
idle / loading / loaded / error
AvatarFallback
className:.primitiv-avatar__fallback
Attribute
Value
When
data-status
idle | loading | loaded | error
idle / loading / loaded / error
Avatar.Fallback
Attribute
Value
When
data-status
idle | loading | loaded | error
idle / 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
Every example below reacts to the density control. Currently showing Styled mode.
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.
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.
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.
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.
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.