Avatar
Round (or rounded / square) user picture with five sizes, an automatic initials fallback when the image fails or is still loading, and a paired AvatarGroup for stacked rows with a built-in +N overflow pill.
Usage
import { Avatar, AvatarGroup } from '@bwo-ui/react';
<Avatar src="/me.jpg" alt="Cristian Cuna" fallback="CC" />
<Avatar fallback="JS" size="lg" />
<AvatarGroup max={3}>
<Avatar fallback="AR" />
<Avatar fallback="MS" />
<Avatar fallback="IL" />
<Avatar fallback="CT" />
<Avatar fallback="DV" />
</AvatarGroup>Sizes
Five sizes β xs (24 px) β xl (80 px). xs is built for inline-with-text density (mentions, comment rails); sm and md are general purpose; lg reads as a profile callout; xl is the hero size for profile pages and onboarding.
xssmmdlgxl<Avatar size="xs" fallback="AR" />
<Avatar size="sm" fallback="AR" />
<Avatar fallback="AR" /> {/* md, default */}
<Avatar size="lg" fallback="AR" />
<Avatar size="xl" fallback="AR" />Shapes
The default circle works for human avatars. rounded (8 px radius) reads better for brand / company / product avatars; square is the editorial choice β handy for project-thumbnail rows that want a hard-edged look.
circleroundedsquare<Avatar shape="circle" src="/u.jpg" fallback="WK" />
<Avatar shape="rounded" src="/u.jpg" fallback="WK" />
<Avatar shape="square" src="/u.jpg" fallback="WK" />Image + fallback
Pass an src and Avatar will render the image once it loads. While loading, the fallback is shown after a brief delay (fallbackDelay, default 300 ms) to avoid a flicker on fast networks. If the image errors, the fallback persists.
The fallback prop accepts anything β initials (the convention), an emoji, an inline SVG icon. If you don't pass it, children are used instead.
imageinitialsbroken β fallbackemojiicon{/* Initials (most common) */}
<Avatar src="/u.jpg" alt="Ana Radu" fallback="AR" />
{/* Loading delay tuned for slow networks */}
<Avatar src="/large.jpg" fallback="AR" fallbackDelay={600} />
{/* Decorative emoji */}
<Avatar fallback="π΅" />
{/* Custom icon */}
<Avatar
fallback={<UserIcon />}
style={{ background: 'var(--bwo-grey-4)' }}
/>For coloured initial bubbles like a directory listing, set the background and color via inline style β Avatar forwards every native span attribute.
<Avatar
fallback="AR"
style={{ background: '#ff481f', color: '#fff' }}
/>AvatarGroup
Stack avatars in an overlapping row. Use max to cap the visible count β any extras collapse into a +N pill. Every member gets a 2 px white ring so the overlap reads cleanly even on tinted backgrounds. The size and shape on the group cascade to children that don't set their own.
{/* No cap β show every avatar */}
<AvatarGroup>
<Avatar fallback="AR" />
<Avatar fallback="MS" />
<Avatar fallback="IL" />
<Avatar fallback="CT" />
</AvatarGroup>
{/* Cap at 3 β extras become +N */}
<AvatarGroup max={3}>
<Avatar fallback="AR" />
<Avatar fallback="MS" />
<Avatar fallback="IL" />
<Avatar fallback="CT" />
<Avatar fallback="DV" />
<Avatar fallback="AP" />
<Avatar fallback="EV" />
</AvatarGroup>
{/* Large size cascades to children */}
<AvatarGroup max={5} size="lg">
{/* β¦ */}
</AvatarGroup>
{/* Explicit total when children are only a sample */}
<AvatarGroup max={2} size="sm" total={28}>
<Avatar fallback="AR" />
<Avatar fallback="MS" />
</AvatarGroup>Hover an avatar in the stack to lift it forward β every member animates independently, which is useful for revealing names on hover via a tooltip layer.
Status indicator (recipe)
Avatars commonly carry a tiny status dot (online / idle / busy / offline). There's no dedicated prop β the avatar's overflow: hidden would clip any absolutely-positioned child anyway β so the canonical recipe is to wrap the avatar in a positioned span and drop a coloured dot at the bottom-right corner. The dot wears a 2 px white ring so it pops off the avatar even on busy photos.
function AvatarWithStatus({ status, ...avatarProps }) {
const color = {
online: '#16a34a',
idle: '#ffc446',
busy: '#ff481f',
offline: 'var(--bwo-grey-3)',
}[status];
return (
<span style={{ position: 'relative', display: 'inline-flex' }}>
<Avatar {...avatarProps} />
<span
aria-hidden
style={{
position: 'absolute',
bottom: 0,
right: 0,
width: 12,
height: 12,
borderRadius: '50%',
background: color,
boxShadow: '0 0 0 2px var(--bwo-white)',
}}
/>
</span>
);
}Comment / message recipe
The classic comment-row layout: md avatar on the left, a name + relative timestamp on the first line, body text on the second. xs works well here when the surrounding text is dense.
Pushed the new IconButton parity branch β variants, sizes, and the toolbar pattern docs are in.
Love the elevation treatment on solid. Going to wire it into the dashboard hero today.
<div style={{ display: 'flex', gap: 12 }}>
<Avatar fallback="AR" style={{ background: '#ff481f', color: '#fff' }} />
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', gap: 8, alignItems: 'baseline' }}>
<span style={{ fontWeight: 600 }}>Ana Radu</span>
<span style={{ fontSize: 11, color: 'var(--bwo-text-body)' }}>2 min ago</span>
</div>
<p>Pushed the new IconButton parity branchβ¦</p>
</div>
</div>Accessibility
- Always pair
srcwith a meaningfulalt. Avatar forwardsaltdirectly to the inner<img>. - When the avatar is purely decorative (e.g. it sits next to the person's name), pass
alt=""so screen readers don't double-announce. - For initials-only avatars, the visible letters carry the meaning β they're inside a
<span>, not an image, so screen readers will read them. If they should be skipped, wrap them in a parent witharia-hidden. - For status indicators, use
aria-hidden="true"on the dot and convey the state with adjacent text (the dot is a visual aid, not the source of truth). The recipe above follows this rule. - AvatarGroup's overflow pill carries
aria-label="{N} more"so screen readers announce the hidden count.
Avatar props
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | β | Image source URL. Falls back to `fallback` while loading or on error. |
alt | string | β | Image alt text. Pass an empty string for decorative avatars (e.g. next to a visible name). |
fallback | ReactNode | β | What to render while the image is loading or has errored β initials, emoji, icon. Falls back to children if omitted. |
fallbackDelay | number | 300 | Milliseconds before the fallback shows while the image is still loading β prevents flicker on fast networks. |
size | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 24 / 32 / 40 / 56 / 80 px square. |
shape | 'circle' | 'rounded' | 'square' | 'circle' | `circle` = 50 % radius, `rounded` = 8 px radius (good for brand avatars), `square` = no radius. |
β¦rest | HTMLAttributes<HTMLSpanElement> | β | All native span attributes β `onClick`, `style`, `aria-*` β are forwarded. Use `style` for coloured initial bubbles. |
AvatarGroup props
| Prop | Type | Default | Description |
|---|---|---|---|
max | number | β | Maximum number of visible avatars. Remaining children collapse into a `+N` overflow pill. Omit to show every child. |
size | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Cascaded to every child avatar (and the overflow pill) that doesnβt set its own size. |
shape | 'circle' | 'rounded' | 'square' | 'circle' | Cascaded to every child avatar that doesnβt set its own shape. |
total | number | β | Explicit total when the rendered children are only a sample of a larger set β the overflow pill will read `+(total β visible)` instead of `+(children β visible)`. |
β¦rest | HTMLAttributes<HTMLDivElement> | β | Native div attributes are forwarded. |
