Skip to content

Avatar

A person or entity reduced to one glyph. Image, initials, fallback order, presence, and stacked groups.

Also called Profile Picture, User Image, Gravatar, Avatar Group — in this system all of them are Avatar.

Live preview

Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.

Playground
AL
Ada LovelaceInitials from the name

The fallback chain

Image, initials, generic glyph. The broken-image icon is the one outcome that must never reach the screen, and it is the browser default.

Image
Initials
GH
Unknown

Sizes

Below 20px initials stop being legible and the avatar becomes a coloured dot. That is the floor — anything smaller should be a Badge.

ALxs
ALsm
ALmd
ALlg
ALxl

Stacked groups

Overlapped by about a third with a ring in the surface colour, so each face stays separable. Past four, count the rest.

ALGHATALGHATKJ+2
ALGHAT+3Ada, Grace, Alan and 3 others

Round for people, square for things

A durable convention: circles are people, rounded squares are organisations, repositories and projects. Mixing them makes a list unscannable.

People
ALGHAT
Organisations
ACG

Interaction states

Every state a user can put this component into, rendered side by side. If a state is missing here, it is missing in production too.

AL
Initials
Broken image
AC
Square
AL
Online
AL
Away
AL
Busy
AL
Offline
Unknown
ALGHAT+3
Stack
AL3
With badge

Anatomy

Every part, every measurement, and the reason it is that number.

ALALGHATKJ+2

A circle carrying initials, a presence dot ringed in the surface colour, and a stack overlapped by a third with a counter for the rest.

  1. ShapeCircle, or 8px square

    Circles are people; rounded squares are organisations and projects. The convention is strong enough that breaking it makes a mixed list unscannable.

  2. InitialsMax 2 characters

    Two at every size. Three are illegible at 24px, and names do not reliably yield three meaningful parts anyway.

  3. Type scale~40% of the diameter

    Scales with the avatar rather than stepping, so a 20px and a 56px avatar look like the same component.

  4. BackgroundDerived from the name

    A hash into the visualisation palette. It aids scanning and is never the identity — two people will collide.

  5. Presence dot~28% of the diameter

    Ringed in the surface colour so it separates from the avatar beneath, and positioned at the bottom-right where a face is least informative.

  6. Stack overlap−33%, with a 2px ring

    A third is enough to read as a group while keeping each face separable. The ring is what stops two adjacent avatars merging.

  7. Stack orderFirst on top

    The first avatar overlaps the second, so reading order matches visual order. Reversed z-index makes the last person appear most prominent.

Design tokens used

Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.

Color

TokenValueUsed for
--p-viz-1 … --p-viz-8—Name-derived background, mixed to 22% alpha so it composes over any surface — categorical, never status
--ds-fg—The initials. The identity hue is a fill; as text it measured 3.9:1
--ds-layer-active—The unknown-entity fallback
--ds-fg-muted—The generic person glyph
--ds-surface—The ring around a presence dot and around stacked avatars
--ds-success—Online presence
--ds-warning—Away presence
--ds-danger—Busy presence

Radius

TokenValueUsed for
full—People
--radius-mdOrganisations and projects

Typography

TokenValueUsed for
weightInitials, which are small and need the weight

Recommended sizes

Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.

SizeHeightTypeWhen to use
xs20px9pxInside a Chip or a dense table cell. The floor for legible initials.
sm24px10pxTable rows and inline mentions.
md32px11pxThe default. List rows and comment authors.
lg40px13pxCard headers and hover cards.
xl56px18pxA profile header, where the avatar is the subject.
Presence dot~28% of diameter—With a 2px ring in the surface colour. Below 20px there is no room for it.

Do

src failed → initials
Fall back to initials, never to a broken imageMost users have no photo and some URLs fail. The browser’s default for both is a broken-image icon, which looks like the product is broken.
AL李MH
Cap initials at two charactersThree are illegible at 24px, and names do not reliably yield three meaningful parts. Two works for almost every name in the world.
ALGHATKJ+2
Ring stacked avatars in the surface colourWithout the ring, two adjacent avatars merge into one shape and the group becomes uncountable.
ALAda Lovelace
Put the name in text, not only in the avatarInitials are recognition aids, not identification. A list of avatars with no names is a puzzle for everyone and unusable without sight.

Don't

Do not use colour as the identityTwo people will hash to the same colour, and colour alone is unreadable for anyone who cannot distinguish it. The initials carry the meaning.
AL14px — unreadable
Do not go below 20pxInitials stop being legible and the avatar becomes a coloured dot that identifies nobody. If that is all the space there is, use text.
ALACGHG
Do not mix circles and squares in one listThe shape carries meaning. A list alternating people and organisations with no shape distinction takes real effort to scan.
ALGHATKJBLMHALGHATKJBLMH
Do not stack more than about fivePast five the overlap hides most of each face and the group stops being countable. Show four and count the rest.

Accessibility

Not a checklist to run at the end. These are the requirements the component was built from.

1.1.1Non-text ContentA1.4.1Use of ColorA1.4.3Contrast (Minimum)AA1.4.11Non-text ContrastAA

Contrast

  • Initials must reach 4.5:1 against their derived background. A palette generated by hashing will produce failures unless the foreground is chosen per swatch.
  • The presence dot needs its 2px ring to reach 3:1 against whatever is beneath — the avatar itself is an unpredictable background.
  • Presence must not rely on colour alone: an accessible label carries the state, since green and amber are indistinguishable for many users at 8px.
  • A stack ring owes 3:1 against the adjacent avatar, or the group merges into one shape.

Keyboard

TabNothing — an avatar is not interactive on its own.
TabReaches it only when it is inside a link or a button, in which case that ancestor carries the name and the focus ring.

Screen readers

  • Avoid announcing the name twice. If the name is beside the avatar, the avatar should be silent.
  • A stack announces once, as a group: "Ada, Grace, Alan and 3 others" beats six separate images.
  • Presence needs a text equivalent. "Ada Lovelace, online" is one announcement; a green dot is none.

Focus & touch

  • An avatar is never focusable on its own. When it opens a profile it is inside a link or a button, and that ancestor carries the accessible name and the focus ring — never the image.
  • An avatar that opens something needs a 44px target, which usually means padding around a 32px avatar rather than a larger avatar. In a stack, only the counter should be interactive — overlapping targets are ambiguous to hit and impossible to describe.
AttributeApplied toNotes
altThe imageThe person’s name, not "avatar" or "profile picture". If the name is already beside it, alt="" and let the text speak.
aria-hiddenA decorative avatarWhen the name is right next to it, the avatar is duplication — hide it rather than announcing the name twice.
aria-labelThe presence dot"Online". A coloured dot with no label is meaningless to a screen reader and to many sighted users.
aria-labelA stack"Ada, Grace, Alan and 3 others". Six unlabelled images is six announcements of nothing.
role="img"An initials avatarWith aria-label naming the person. Two letters read literally are "A, L", which identifies nobody.

Code

Example usage

tsx
1import { Avatar, AvatarStack } from '@/ui/Display'23<Avatar name="Ada Lovelace" src={user.avatarUrl} size="md" status="online" />45<AvatarStack people={reviewers} max={4} size="sm" />67// The fallback chain IS the component. A broken-image icon must never reach8// the screen, and it is the browser's default for a failed src.9function Avatar({ name, src, size }: AvatarProps) {10  const [failed, setFailed] = React.useState(false)11  const showImage = src && !failed1213  return (14    <span role="img" aria-label={name} className={cls(size)}>15      {showImage ? (16        <img src={src} alt="" onError={() => setFailed(true)} />17      ) : (18        initials(name) || <UserIcon aria-hidden />19      )}20    </span>21  )22}2324// Names do not split reliably. "van der Berg", "李", a mononym — take the25// first grapheme of the first and last parts and accept one character.26function initials(name: string) {27  const parts = name.trim().split(/\s+/).filter(Boolean)28  if (parts.length === 0) return ''29  const first = [...parts[0]][0] ?? ''30  const last = parts.length > 1 ? ([...parts[parts.length - 1]][0] ?? '') : ''31  return (first + last).toUpperCase()32}3334// Colour aids scanning; it is never the identity. Two people will collide.35function hue(name: string) {36  let h = 037  for (const ch of name) h = (h * 31 + ch.codePointAt(0)!) % VIZ_COLORS.length38  return VIZ_COLORS[h]39}

Framework-free HTML

html
<!-- With an image: the name is the alt text, never "avatar". -->
<span class="ds-avatar ds-avatar--md">
  <img src="/u/ada.jpg" alt="Ada Lovelace" />
</span>

<!-- Initials: role="img" with a label, because "A L" identifies nobody. -->
<span class="ds-avatar ds-avatar--md" role="img" aria-label="Grace Hopper"
      style="--avatar-bg: color-mix(in oklab, var(--p-viz-3) 22%, transparent)">
  <span aria-hidden="true">GH</span>
</span>

<!-- Beside the name, the avatar is duplication: hide it. -->
<div class="ds-row">
  <span class="ds-avatar" aria-hidden="true"><img src="/u/ada.jpg" alt="" /></span>
  <span>Ada Lovelace</span>
</div>

<!-- Presence needs a text equivalent. -->
<span class="ds-avatar">
  <img src="/u/ada.jpg" alt="Ada Lovelace" />
  <span class="ds-avatar__status" data-status="online">
    <span class="sr-only">Online</span>
  </span>
</span>

<!-- One announcement for the group, not six. -->
<span class="ds-avatar-stack" role="img"
      aria-label="Ada, Grace, Alan and 3 others">…</span>

CSS

css
.ds-avatar {
  position: relative;
  display: inline-grid;
  place-items: center;
  flex: 0 0 auto;
  border-radius: 999px;              /* circles are people */
  /* An alpha tint, so the same avatar lifts off a card and a drawer as
     readily as off the page. Mixing toward a surface token freezes it at one
     rung and inverts everywhere else. */
  background: var(--avatar-bg, var(--ds-layer-active));

  /* The hue identifies the person from the FILL. Read as text it measures
     3.9-4.4:1 across the eight, because the viz ramp is tuned for fills. */
  color: var(--ds-fg);
  font-weight: 600;                  /* initials are small and need it */
  overflow: hidden;
  user-select: none;
}

/* Rounded squares are organisations, repositories and projects. */
.ds-avatar--square { border-radius: var(--radius-md); }

/* Scales with the avatar rather than stepping, so 20px and 56px read as the
   same component. */
.ds-avatar { font-size: calc(var(--avatar-size) * 0.4); }

.ds-avatar img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;                 /* never squash a non-square photo */
}

/* The ring separates the dot from an unpredictable background. */
.ds-avatar__status {
  position: absolute;
  inset-block-end: 0;
  inset-inline-end: 0;
  inline-size: calc(var(--avatar-size) * 0.28);
  block-size: calc(var(--avatar-size) * 0.28);
  border-radius: 999px;
  box-shadow: 0 0 0 2px var(--ds-surface);
}
.ds-avatar__status[data-status='online'] { background: var(--ds-success); }
.ds-avatar__status[data-status='away']   { background: var(--ds-warning); }
.ds-avatar__status[data-status='busy']   { background: var(--ds-danger); }

/* A third of overlap: enough to read as a group, enough to stay separable. */
.ds-avatar-stack { display: inline-flex; }
.ds-avatar-stack > * + * { margin-inline-start: -33%; }
.ds-avatar-stack > * {
  box-shadow: 0 0 0 2px var(--ds-surface);
  /* First on top, so reading order matches visual order. */
  position: relative;
}

Component API

Avatar

PropTypeDefaultDescription
name*string—Drives the initials, the derived colour and the accessible label. Required even when an image is present.
srcstring—Falls back to initials on error. The onError handler is the main path for most users.
size'xs' | 'sm' | 'md' | 'lg' | 'xl''md'20px to 56px. Below xs, initials stop being legible.
squarebooleanfalseFor organisations, repositories and projects. Circles are for people.
status'online' | 'away' | 'busy' | 'offline'—Adds a ringed presence dot with a visually hidden label.

AvatarStack

PropTypeDefaultDescription
people*{ name: string; src?: string }[]—In reading order. The first is rendered on top.
maxnumber4Past this, a "+n" counter. Five is the practical ceiling before faces stop being separable.
size'xs' | 'sm' | 'md''sm'Stacks are dense by nature; larger sizes overlap too much to read.

Notes

Professional tips

  • Serve avatars at twice their display size for high-density screens and no larger. A 512px image rendered at 32px is 250 KB thrown away.
  • Cache the derived colour per user id rather than recomputing the hash on every render — it also keeps the colour stable if the display name changes.
  • Show the full name on hover via a Tooltip in dense lists, and always have the name in text somewhere for touch users.
  • For organisations, prefer a logo over initials where one exists, but keep the same square shape so the list stays scannable.
  • Uploaded avatars should be cropped to a square on the client before upload. A component that squashes non-square photos is a component nobody trusts with their face.

Performance

  • Lazy-load avatars below the fold, but never the ones in the viewport — a page of empty circles filling in is worse than a slightly later paint.
  • Use a sprite or a single request for a stack rather than six separate image requests in a table with fifty rows.
  • Render initials as text, not as generated SVG or canvas. Text is cached, scalable, selectable by the accessibility tree and free.
  • Set explicit width and height so a late-loading image does not shift the row it sits in.

Common mistakes

  • No onError handler, so a failed image shows the browser’s broken-image icon.
  • Three or more initials, illegible at small sizes.
  • alt="avatar" instead of the person’s name.
  • The name announced twice — once by the avatar and once by the text beside it.
  • Colour as the only identity signal, which collides and fails in greyscale.
  • Avatars below 20px, where initials become an unreadable smudge.
  • A presence dot with no accessible label.
  • object-fit missing, squashing every non-square photo.

Real-world recommendations

  • Most users never upload a photo. The initials path is the main path, not the fallback, and it deserves the majority of the design attention.
  • Name-derived colours are genuinely useful for scanning a long activity feed — but only alongside the initials, never instead of them.
  • Stacked avatars work best at three or four. Past that people stop counting and start reading it as "a lot", at which point the number is doing the work.
  • Square avatars for organisations is one of the most reliable conventions in product UI. Users pick it up without being told, and breaking it is immediately disorienting.