Overview
An avatar identifies a person, an organisation, or a system actor at a glance. It's the smallest piece of identity in the product — a 24 px chip in a comment row, a 72 px hero on a profile page, or anything in between. Five sizes, four content shapes (image, initial, icon, fallback), three optional status badges. The component does identification, not branding; it stays quiet and consistent across surfaces.
Initials are single characters only
Per the canonical spec, an initial avatar carries one uppercase letter — “A”, “B”, “C” — never two. Two initials force the type to overflow at smaller sizes. For multi-word names, take the first letter of the first name and trust the surrounding label to carry the rest.
Anatomy
Three parts — a circular surface, the content inside, and an optional status badge anchored at the top-right corner. Everything beyond that (a name, a role, a timestamp) is the responsibility of the surrounding row, not the avatar itself.
Surface
Round container. Default fill is
background.accent.neutral.subtler(#eeeeef). Categorical avatars can use anyaccent.<hue>.subtlerto encode role or method (the transfers list uses one per payment method).Content
An image (object-cover crop), a single uppercase initial, an icon, or the fallback person glyph. The four shapes never combine on one avatar.
Status badge (optional)
16 px circle (md size) anchored 15% inset at top-right, with a 1.5 px background-coloured ring so it reads against any surface. Carries a 12 px white glyph — check, warning, or priority-high. Sized proportionally to the avatar.
Sizes
Five sizes — 24 / 32 / 40 / 56 / 72 px. The default is md (40 px) and covers activity feed rows, transfer items, and most product list contexts. Pick a size that matches the type next to the avatar, never the surrounding container.
xs · 24 px
AInline metadata — author chips, comment timestamps, dense rows where the avatar is identification, not feature.
sm · 32 px
ACompact list rows, table cells, dropdown items where the avatar pairs with single-line text.
md · 40 px
AThe default. Activity feed rows, transfer items, sidebar identities. Pairs with two-line content.
lg · 56 px
ADetail headers, profile cards, sidebar account chip. The first thing the eye lands on.
xl · 72 px
AProfile pages, identity hero blocks, marketing testimonials. Reserve for surfaces about the person.
Content shapes
Four content shapes, ordered by fidelity: image > initial > icon > fallback. Reach for the highest-fidelity shape your data actually supports — don't default to a fallback when you have a name to derive an initial from.
Initial
Single uppercase letter. Default for known people without a photo.
Icon · default
Falls through to a person glyph when there's no name and no image. Use as a placeholder, not as identity.
Image
Square crop fitted to a circle. Highest fidelity; reserve for real photography of real people.
Question mark
Use when the identity is genuinely unknown — anonymous reviewer, placeholder before sign-in, audit-log entry from a system actor.
Status badge
Three status badges — success (green), warning (yellow), danger (red). Each carries a single white glyph at a size proportional to the avatar. The badge is decorative; the meaning belongs to the copy that names the state. A green dot alone is decoration; a green dot next to “Online” is information.
Success
Online, verified, paid, ready. Pair with copy that names the state — “Online”, “Verified”.
Warning
Action needed, expiring, verification pending. Doesn't block the user; nudges them.
Danger
Failed payment, blocked account, hard error attached to this person. Use sparingly; the page should also explain why.
Status badges aren't notification dots
The avatar status badge communicates a property of the person/entity (online, verified, blocked). For a notification indicator (unread comment, new mention, action required), use the Indicator component on the surface that carries the count, not the avatar.
Group
When several people share a context — a project team, the reviewers on a transfer, a multi-author commit — render the avatars as an overlapping stack. Cap visible avatars at four; everything past that becomes a +N pill at the end of the stack.
Group · 4 visible
Reviewing this transfer
Group · overflow
12 people on this prototype
Cap visible avatars at 4. Overflow becomes a +N pill with the remaining count. Past 99, render “+99” and link to the full list.
Group · sizes
Avatar block
Avatar + name + secondary metadata (role, timestamp, status) is the most common pairing in the product. The block isn't a separate component — it's a layout pattern: 12 px gap, two-line stack on the right, name in foreground semibold, meta in 12 px muted.
Anders Holm
Repo owner
Mira Berg
Last commit · 2 hours ago
Lukas Sand
Reviewer
Behavior
- Falls through gracefully. When the image fails to load, the avatar falls back to the initial. When there's no initial, it falls back to the icon. When there's no icon, it shows the person glyph. The component never renders empty.
- Single character only. Even when the source name is “Anders Holm”, the initial is “A”. Two letters force the type to overflow at smaller sizes; the surrounding label carries the full name.
- Square crops only. Images are cropped to the centre square via
object-coverbefore being clipped to the circle. Don't feed pre-cropped non-square images — the avatar will distort. - Group overlap is fixed at 30%. Each avatar in the group sits 30% behind the previous one. Less and the stack reads as separate avatars; more and the faces disappear behind each other.
- Avatars don't scale on hover. They're identification, not interaction. If the avatar is a link or button, the wrapping element handles the hover treatment — not the avatar itself.
Accessibility
- Decorative by default. When the avatar pairs with a visible name (the avatar block pattern), the avatar itself is
aria-hidden="true"and the name carries identity. Don't announce the same person twice. - Standalone gets a label. When the avatar is the only thing identifying the person (a comment thread author chip, a hover card trigger), set
role="img"andaria-labelwith the full name. - Image
altstays empty on the underlying<img>— the wrappingaria-labelalready names the person. A screen reader doesn't need to hear “photo of Mira” followed by “Mira”. - Status badgesare paired with copy elsewhere on the surface — “Mira (online)” in the label, never colour alone. The badge itself carries
aria-hidden="true"because the meaning lives in the text. - Groups carry a single label on the wrapping
role="group"— “Project team, 12 people” — instead of announcing each avatar separately. Tab moves through interactive avatars inside the group; non-interactive avatars are skipped. - Touch target:for interactive avatars (a chip that opens a profile, a clickable group member), make the actual hit area at least 44 px regardless of the avatar's visual size. The 24 px and 32 px sizes can't be the entire click target.
Code
One Avatar component handles every content shape. The AvatarGroup wrapper handles the stack + overflow.
tsx
import { Avatar, AvatarGroup } from "@flatpay-dk/ui";
// Image — the highest-fidelity shape
<Avatar size="md" src="/avatars/mira.jpg" label="Mira Berg" />
// Initial — derived from the name's first letter, single character only
<Avatar size="md" initial={name[0]} label={name} />
// Icon — for system actors, anonymous users, role-not-person
<Avatar size="md" icon={<RobotIcon />} label="Automation system" />
// Fallback — when there's no name, no image, no role
<Avatar size="md" label="Unknown contributor" />
// Status badge — pairs with copy that names the state
<Avatar size="md" initial="M" status="success" label="Mira (online)" />
// Group with overflow
<AvatarGroup size="md" max={4} total={12} label="Project team">
{members.map((m) => (
<Avatar key={m.id} initial={m.name[0]} src={m.photo} label={m.name} />
))}
</AvatarGroup>
// Block pattern — avatar + name + meta
<div className="flex items-center gap-3">
<Avatar size="md" initial="A" label="Anders Holm" />
<div>
<p className="text-sm font-semibold">Anders Holm</p>
<p className="text-xs text-muted-foreground">Repo owner</p>
</div>
</div>Best practices
Avatars carry identity at a glance. The defaults below keep them legible and consistent across hundreds of rows.
Anders Holm
Repo owner
Do
Use a single uppercase initial when there's no photo. The surrounding label carries the full name.
Anders Holm
Repo owner
Don't
Don't pack two letters in. AA, MR, or BG overflow the smaller sizes and read as code, not identity.
Mira Berg
Online · 2 min ago
Do
Status badge pairs with named copy — 'Online', 'Verified', 'Action needed'. The dot doubles up; it doesn't replace.
Mira Berg
Don't
Don't carry meaning in the badge alone. A green dot with no label leaves screen-reader users with nothing.
Do
Cap visible avatars at four; collapse the rest into a +N pill. Long stacks read as a count, not as people.
Don't
Don't render every member of a 30-person team as a separate avatar. The stack becomes noise; nobody reads past the first three.
Props
Avatar
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | 24 / 32 / 40 / 56 / 72 px. Pick by the type next to the avatar, not the container around it. |
| src | string | — | Image URL. Wins over initial / icon. Cropped via object-cover before clipping to the circle. |
| initial | string | — | Single uppercase letter. Multi-character strings are truncated to the first character. |
| icon | ReactNode | — | Custom icon for system actors / non-person identities. Falls through to a default person glyph if none is set and no initial / src is provided. |
| status | "success" | "warning" | "danger" | — | Optional badge anchored top-right. Pairs with copy that names the state — never carries meaning alone. |
| label | string | — | Accessible name. Required when the avatar stands alone; can be omitted when paired with a visible label that already names the person. |
| background | string | — | Custom surface colour. Defaults to background.accent.neutral.subtler. Use accent.<hue>.subtler for categorical avatars (transfer methods, label colours). |
AvatarGroup
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Applied to every avatar in the group. Don't mix sizes inside a single group. |
| max | number | 4 | Maximum visible avatars. Children past this index collapse into a +N pill. |
| total | number | — | Total member count. Used to compute the +N pill when only a sample is rendered as children. |
| label | string | — | Group label — 'Project team', 'Reviewers', 'Online now'. Read aloud once for the whole group. |
| children | ReactNode | — | Avatar nodes. Render order is left-to-right; the first avatar sits in front. |