Skip to content

Avatar

One span. A photo goes in through src, a framework image goes in as a child, and initials fall out of letters — the stylesheet clips whichever one arrives, at any depth, with no wrapper and no cloning. The presence dot is a pseudo-element on the box itself, so nothing can clip it either, and a group is a flex row with one negative margin.

  • Stable
  • React 19
  • One element
  • Zero drawing JS
DV AM
Code
<Avatar src={user.photo} />
<span class="wayra-avatar" ><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%233d6dd8%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23eef2ff%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23eef2ff%22%2F%3E%3C%2Fsvg%3E" alt="" loading="lazy" decoding="async" /></span>

That is the whole common case: a 48px circle around a plain <img>, lazy-loaded and decoded off the main thread. The image has no alternative text, and that is deliberate. An avatar almost always sits next to the person’s name, and a second copy of the name only makes the row longer to listen to. Pass alt when the avatar stands alone and nothing else identifies it:

<Avatar src={user.photo} alt={user.name} />

They are tried in order — src, then children, then letters — and only one of them ever draws.

From src From a framework image DV
Code
<Avatar src="/daniel.jpg" alt="Daniel Vela" />
<Avatar>
<Image src={daniel} alt="Daniel Vela" width={96} height={96} />
</Avatar>
<Avatar letters="DV" alt="Daniel Vela" />
<span class="wayra-avatar" ><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%233d6dd8%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23eef2ff%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23eef2ff%22%2F%3E%3C%2Fsvg%3E" alt="From src" loading="lazy" decoding="async" /></span>
<span class="wayra-avatar" ><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%230e7a52%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23ecfdf5%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23ecfdf5%22%2F%3E%3C%2Fsvg%3E" alt="From a framework image" loading="lazy" decoding="async" /></span>
<span class="wayra-avatar" role="img" aria-label="Daniel Vela" data-placeholder="true" >DV</span>

src renders a plain <img>. The library takes no framework dependency, so if you want next/image, unpic, a <picture> with three sources or an inline <svg>, you pass it as a child instead — it is rendered exactly as written, with nothing around it:

.wayra-avatar :is(img, picture, video, svg) {
display: block;
inline-size: 100%;
block-size: 100%;
border-radius: var(--wayra-avatar-radius);
object-fit: cover;
}

That one rule is why there is no wrapper element and no cloneElement. The corner comes from the variable rather than from inherit, so the <img> inside a <picture> — two levels down — is clipped exactly like a bare one. object-fit: cover means a rectangular photo is centred and cropped rather than squashed.

With no src and no child, letters draws the placeholder.

A src that fails to load counts as no src: the avatar drops the broken-image icon and draws the next branch instead. An empty string never reaches an <img> at all, and a new src is tried again. So pass letters next to a photo that might be missing, and the initials are the fallback:

<Avatar src={user.photo} letters={user.initials} alt={user.name} />

Your own onError still runs when the image fails, before the fallback draws.

color picks the semantic role behind the placeholder fill and the ring. It reaches the DOM as data-color, and the stylesheet turns that into the role pair.

PR SE AC NE IN SU WA ER
Code
<Avatar letters="PR" alt="primary" color="primary" />
<Avatar letters="SE" alt="secondary" color="secondary" />
<Avatar letters="AC" alt="accent" color="accent" />
<Avatar letters="NE" alt="neutral" color="neutral" />
<Avatar letters="IN" alt="info" color="info" />
<Avatar letters="SU" alt="success" color="success" />
<Avatar letters="WA" alt="warning" color="warning" />
<Avatar letters="ER" alt="error" color="error" />
<span class="wayra-avatar" role="img" aria-label="primary" data-placeholder="true" data-color="primary">PR</span>
<span class="wayra-avatar" role="img" aria-label="secondary" data-placeholder="true" data-color="secondary">SE</span>
<span class="wayra-avatar" role="img" aria-label="accent" data-placeholder="true" data-color="accent">AC</span>
<span class="wayra-avatar" role="img" aria-label="neutral" data-placeholder="true" data-color="neutral">NE</span>
<span class="wayra-avatar" role="img" aria-label="info" data-placeholder="true" data-color="info">IN</span>
<span class="wayra-avatar" role="img" aria-label="success" data-placeholder="true" data-color="success">SU</span>
<span class="wayra-avatar" role="img" aria-label="warning" data-placeholder="true" data-color="warning">WA</span>
<span class="wayra-avatar" role="img" aria-label="error" data-placeholder="true" data-color="error">ER</span>

The fill is --wayra-color-<role> and the letters are its -content counterpart. That is the whole two-token contract the library is built on, and it is why the initials clear 4.5:1 by construction rather than by luck: a role that failed against its own content token would never have shipped. A theme moves both halves at once, so a placeholder never needs retuning.

The ring reads the same pair, which is why an avatar has one color and not a separate borderColor.

xs sm md lg xl
Code
<Avatar src="/daniel.jpg" alt="xs" size="xs" />
<Avatar src="/daniel.jpg" alt="sm" size="sm" />
<Avatar src="/daniel.jpg" alt="md" size="md" />
<Avatar src="/daniel.jpg" alt="lg" size="lg" />
<Avatar src="/daniel.jpg" alt="xl" size="xl" />
<span class="wayra-avatar" data-size="xs"><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%233d6dd8%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23eef2ff%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23eef2ff%22%2F%3E%3C%2Fsvg%3E" alt="xs" loading="lazy" decoding="async" /></span>
<span class="wayra-avatar" data-size="sm"><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%233d6dd8%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23eef2ff%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23eef2ff%22%2F%3E%3C%2Fsvg%3E" alt="sm" loading="lazy" decoding="async" /></span>
<span class="wayra-avatar" data-size="md"><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%233d6dd8%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23eef2ff%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23eef2ff%22%2F%3E%3C%2Fsvg%3E" alt="md" loading="lazy" decoding="async" /></span>
<span class="wayra-avatar" data-size="lg"><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%233d6dd8%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23eef2ff%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23eef2ff%22%2F%3E%3C%2Fsvg%3E" alt="lg" loading="lazy" decoding="async" /></span>
<span class="wayra-avatar" data-size="xl"><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%233d6dd8%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23eef2ff%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23eef2ff%22%2F%3E%3C%2Fsvg%3E" alt="xl" loading="lazy" decoding="async" /></span>

The ladder walks --wayra-size-selector, the unit the small controls share, on multipliers of its own. It sits below the radial ring at every step, because an avatar lives in a chat row or a member list rather than on a dashboard.

Size Box Initials Status dot
xs 24px 9.6px 5.3px
sm 32px 12.8px 7px
md 48px 19.2px 10.6px
lg 64px 25.6px 14.1px
xl 80px 32px 17.6px

Everything in the box is a fraction of the box: the initials are size / 2.5 and the dot is 22% of it, so one number scales the whole drawing. Measured with the default --wayra-size-selector of 0.25rem; a theme that changes that token moves the ladder with it.

There is no size={40} — no prop in this library takes a CSS length. A one-off measurement is a custom property on the instance, and because everything derives from it, the result is still proportional:

<Avatar src={user.photo} style={{ '--wayra-avatar-size': '2.75rem' }} />
Circle Square DV
Code
<Avatar src="/daniel.jpg" alt="Circle" />
<Avatar src="/ana.jpg" alt="Square" shape="square" />
<Avatar letters="DV" alt="Daniel Vela" shape="square" />
<span class="wayra-avatar" ><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%233d6dd8%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23eef2ff%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23eef2ff%22%2F%3E%3C%2Fsvg%3E" alt="Circle" loading="lazy" decoding="async" /></span>
<span class="wayra-avatar" data-shape="square"><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%230e7a52%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23ecfdf5%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23ecfdf5%22%2F%3E%3C%2Fsvg%3E" alt="Square" loading="lazy" decoding="async" /></span>
<span class="wayra-avatar" role="img" aria-label="Daniel Vela, square" data-placeholder="true" data-shape="square">DV</span>

A circle is the pill radius on a square box, so there is no 50% anywhere in this stylesheet — one private token owns every round corner in the library. A square is size / 6, proportional rather than fixed: a single flat radius would round the 24px step into a capsule and leave the 80px step looking sharp.

Online Offline DV Online, square
Code
<Avatar src="/daniel.jpg" alt="Online" status="online" />
<Avatar src="/ana.jpg" alt="Offline" status="offline" />
<Avatar letters="DV" alt="Daniel Vela" status="online" />
<Avatar src="/lu.jpg" alt="Online, square" shape="square" status="online" />
<span class="wayra-avatar" data-status="online"><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%233d6dd8%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23eef2ff%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23eef2ff%22%2F%3E%3C%2Fsvg%3E" alt="Online" loading="lazy" decoding="async" /></span>
<span class="wayra-avatar" data-status="offline"><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%230e7a52%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23ecfdf5%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23ecfdf5%22%2F%3E%3C%2Fsvg%3E" alt="Offline" loading="lazy" decoding="async" /></span>
<span class="wayra-avatar" role="img" aria-label="Daniel Vela" data-placeholder="true" data-status="online">DV</span>
<span class="wayra-avatar" data-shape="square" data-status="online"><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%236b31c9%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23f5f3ff%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23f5f3ff%22%2F%3E%3C%2Fsvg%3E" alt="Online, square" loading="lazy" decoding="async" /></span>

The dot is a pseudo-element on the avatar itself, and that is the reason this component has no inner frame: a wrapper that clipped the image would clip the dot with it. It is outlined in --wayra-avatar-ring, the surface colour, so it stays readable on top of any photo.

On a circle the dot sits inside the box, because a round box hides its own corner. On a square it moves out onto the corner the rounding would have taken, which is the only rule in the file that keys on two attributes at once.

Active Active, success DV
Code
<Avatar src="/daniel.jpg" alt="Active" ring />
<Avatar src="/ana.jpg" alt="Active" ring color="success" />
<Avatar letters="DV" alt="Daniel Vela" ring color="accent" />
<span class="wayra-avatar" data-ring="true"><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%233d6dd8%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23eef2ff%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23eef2ff%22%2F%3E%3C%2Fsvg%3E" alt="Active" loading="lazy" decoding="async" /></span>
<span class="wayra-avatar" data-ring="true" data-color="success"><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%230e7a52%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23ecfdf5%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23ecfdf5%22%2F%3E%3C%2Fsvg%3E" alt="Active, success" loading="lazy" decoding="async" /></span>
<span class="wayra-avatar" role="img" aria-label="Daniel Vela" data-placeholder="true" data-ring="true" data-color="accent">DV</span>

Two layers of box-shadow: the surface colour first at 2px, then the role at 4px. The gap is what keeps the ring reading as a ring rather than as a thick border, and because it is a shadow it costs no layout — a ringed avatar is exactly the same size as a plain one and a row of them does not shift when one becomes active.

AvatarGroup overlaps its members and cuts each one out of the one behind it.

Ana Beto Carla +9
Code
<AvatarGroup>
<Avatar src="/ana.jpg" alt="Ana" />
<Avatar src="/beto.jpg" alt="Beto" />
<Avatar src="/carla.jpg" alt="Carla" />
<Avatar letters="+9" alt="9 more people" />
</AvatarGroup>
<div class="wayra-avatar-group" >
<span class="wayra-avatar" ><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%233d6dd8%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23eef2ff%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23eef2ff%22%2F%3E%3C%2Fsvg%3E" alt="Ana" loading="lazy" decoding="async" /></span>
<span class="wayra-avatar" ><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%230e7a52%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23ecfdf5%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23ecfdf5%22%2F%3E%3C%2Fsvg%3E" alt="Beto" loading="lazy" decoding="async" /></span>
<span class="wayra-avatar" ><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%238a4b0d%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23fffbeb%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23fffbeb%22%2F%3E%3C%2Fsvg%3E" alt="Carla" loading="lazy" decoding="async" /></span>
<span class="wayra-avatar" role="img" aria-label="9 more people" data-placeholder="true" >+9</span>
</div>

The overlap is --wayra-avatar-size * -0.25 — a quarter of each member’s own size, so a row of mixed sizes still overlaps in proportion. The halo is a 3px shadow in the surface colour, and a member carrying a ring is excluded from it: the ring’s inner layer is already that separation.

Ana Beto Carla Dani +2
Code
<AvatarGroup max={4}>
{team.map((member) => (
<Avatar key={member.id} src={member.photo} alt={member.name} size="sm" />
))}
</AvatarGroup>
<div class="wayra-avatar-group" >
<span class="wayra-avatar" data-size="sm"><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%233d6dd8%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23eef2ff%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23eef2ff%22%2F%3E%3C%2Fsvg%3E" alt="Ana" loading="lazy" decoding="async" /></span>
<span class="wayra-avatar" data-size="sm"><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%230e7a52%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23ecfdf5%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23ecfdf5%22%2F%3E%3C%2Fsvg%3E" alt="Beto" loading="lazy" decoding="async" /></span>
<span class="wayra-avatar" data-size="sm"><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%238a4b0d%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23fffbeb%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23fffbeb%22%2F%3E%3C%2Fsvg%3E" alt="Carla" loading="lazy" decoding="async" /></span>
<span class="wayra-avatar" data-size="sm"><img src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20fill%3D%22%236b31c9%22%2F%3E%3Ccircle%20cx%3D%2232%22%20cy%3D%2225%22%20r%3D%2211%22%20fill%3D%22%23f5f3ff%22%2F%3E%3Cpath%20d%3D%22M8%2064c0-13%2011-23%2024-23s24%2010%2024%2023Z%22%20fill%3D%22%23f5f3ff%22%2F%3E%3C%2Fsvg%3E" alt="Dani" loading="lazy" decoding="async" /></span>
<span class="wayra-avatar" role="img" aria-label="+2" data-placeholder="true" data-color="neutral" data-size="sm">+2</span>
</div>

Above max, the rest collapse into a counter drawn as an ordinary avatar. It is always neutral, so it never reads as one more person, and it takes the size of the last member it follows. Its label is +N — a number rather than a sentence, because it is the one string this component would otherwise have to translate. If you want words, drop max and write the last member yourself, as in the example above it.

Members behind a flag cost nothing. null and false children are dropped before anything is counted, so a guest who is not there cannot push a face into the counter:

<AvatarGroup max={4}>
{team.map((member) => <Avatar key={member.id} src={member.photo} alt={member.name} />)}
{guest && <Avatar src={guest.photo} alt={guest.name} />}
</AvatarGroup>

The class plus the data-* attributes is the public API, and the stylesheet is written against it. Every preview on this page is hand-authored HTML — there is no React on it:

<span class="wayra-avatar" data-color="primary" data-size="md" data-shape="circle"
data-status="online">
<img src="/daniel.jpg" alt="" loading="lazy" decoding="async" />
</span>
<span class="wayra-avatar" role="img" aria-label="Daniel Vela"
data-placeholder="true" data-color="primary" data-size="md" data-shape="circle">
DV
</span>

data-placeholder="true" is what fills the box; without it a span with letters in it is just a transparent box with centred text. In hand-written markup you also write the role="img" and the aria-label yourself — the component only emits them on the branch that generates them.

Three custom properties are yours to set:

Property Default What it does
--wayra-avatar-size calc(var(--wayra-size-selector) * 12) The box in both axes. The initials and the dot are fractions of it.
--wayra-avatar-radius 9999px, or calc(size / 6) when data-shape="square" The corner, applied to the box and to every image inside it.
--wayra-avatar-ring var(--wayra-color-base-100) The surface the avatar is cut out of: dot outline, ring, and group halo.

Set --wayra-avatar-ring wherever an avatar sits on something other than the page background — a card in base-200, a coloured header — and the dot outline, the ring’s inner layer and the group halo all follow at once.

AvatarProps extends ComponentProps<"span"> minus color, so id, style, ref, className, title and every data-* land on the box.

Prop Type Default Description
color WayraColor "primary" Role behind the placeholder fill and the ring. Emitted as data-color.
size WayraSize "md" Step on the avatar ladder, 24px to 80px.
shape "circle" | "square" "circle" Round or proportionally rounded corners.
status "online" | "offline" — Draws the presence dot. Never the only signal.
ring boolean false Draws the two-layer ring in the role colour.
src string — Source for the plain <img> path. Wins over children and letters unless it is empty or fails to load.
alt string — Image alternative text, or the accessible name of a placeholder.
letters string — Initials for the placeholder. Usually one or two.
children ReactNode — A framework image, rendered with no wrapper.
unstyled boolean false Drops the wayra-avatar class, keeps every data-*.

AvatarGroupProps extends ComponentProps<"div">.

Prop Type Default Description
max number — How many members to show before the rest collapse into +N.
children ReactNode — The avatars. Required. null and false are dropped before counting.
unstyled boolean false Drops the wayra-avatar-group class from the row.

Three things the legacy component had are deliberately gone. There is no numeric size, for the reason under Sizes. There is no border plus borderColor pair: they were two props where one axis exists, and setting a colour without a width did nothing at all — ring and color cannot be put into that state. And there is no innerClassName, because there is no inner element left to name.

  • An image avatar is decorative by default. No alt means alt="", which keeps a redundant reading of a name that is already on screen out of the accessibility tree. It is a real empty alt, not aria-hidden on top of one — the empty attribute is what axe and every screen reader agree on.
  • A standalone avatar needs a name. If the avatar is the only thing identifying the person — a row of faces with no text — pass alt.
  • A placeholder is an image. It carries role="img" and a computed aria-label, so the initials are announced as the person, not spelled out as two letters. The label is aria-label, then alt, then the letters themselves; the last of those is legal but poor, so pass one of the first two. With none of the three there is nothing to name, and the box drops the role and stays out of the tree, as an image with an empty alt does.
  • Naming precedence is yours to override. aria-label on the component wins over alt, which is how one avatar in a list carries a longer name than the rest.
  • The status dot is silent. It is a pseudo-element: it has no text and no role, and colour alone never carries meaning. Put the state in the row as text.
  • Contrast is the role contract. The initials are the role’s -content token on its solid, measured at 4.5:1 or better in both schemes in every shipped theme but wayra, the documented exception — the one palette that trades a measured ratio for reproducing its source untouched. The dot outline is the surface, so the dot is separated from the photo by shape rather than by hue.
  • Windows high contrast. The placeholder keeps an outline where its fill flattens, and the dot opts out to hold a system colour. Both states become the same mark there — one more reason the state has to be written somewhere too.
  • Loading. Images ship loading="lazy" and decoding="async". A face above the fold that must load eagerly goes in through children, where you control the tag.

Do

Daniel Vela

The name is on the page, so the image is decorative: alt="". A screen reader reads the row once.

Don't

Daniel VelaDaniel Vela

The same name twice in a row — once from the image, once from the text beside it.

Do

Ana MolinaAna Molina — Online

The dot is the fast signal; the word is the one that survives high contrast, greyscale and a screen reader.

Don't

Ana MolinaAna Molina

Presence carried by a green dot and nothing else. Half the audience never receives it.