Avatar

User image with a text fallback — two variants, nine sizes, color theming.

A square image slot with a text fallback underneath. The image paints over the fallback and hides itself when it has nothing to paint, revealing the initials. Class + data-attribute API mirrors the Badge: one .x-avatar class plus data-variant, data-size, data-accent.

Open in Storybook with controls ↗

Install

import "elements-kit/ui/styles.css";

import "elements-kit/ui/styles/palette/gray.css";
import "elements-kit/ui/styles/neutral/gray.css";
import "elements-kit/ui/styles/palette/iris.css";
import "elements-kit/ui/styles/accent/iris.css";

import "elements-kit/ui/avatar/avatar.css";

API

<span class="x-avatar" data-size="3" data-variant="soft">
  <span class="x-avatar-fallback">WB</span>
  <img class="x-avatar-image" src="/wael.jpg" alt="" onerror="this.remove()" />
</span>
AttributeValues
data-variantsoft (default), solid
data-size1–9 (default 3) — on .x-avatar, the box size
data-size1–3 — on .x-avatar-fallback, the type ratio
data-accentany imported color scale (iris, blue, amber, …)
data-high-contrastmodifier — boosts contrast against the page background
data-statuserror — on .x-avatar-image, hides it so the fallback shows
ClassRole
.x-avatarroot — owns size, radius, and clipping
.x-avatar-fallbackbottom layer — centered initials
.x-avatar-imagetop layer — object-fit: cover

The fallback's own data-size picks how much of the box the text fills — a ratio, not a step off the type scale, so it holds at every avatar size:

data-sizefont-sizefor
130% of the boxthree letters or more
2 (default)40% of the boxtwo initials
350% of the boxa single letter
<!-- default — sized for two initials -->
<span class="x-avatar" data-size="5">
  <span class="x-avatar-fallback">WB</span>
</span>

<!-- one letter, filling more of the box -->
<span class="x-avatar" data-size="5">
  <span class="x-avatar-fallback" data-size="3">W</span>
</span>

<!-- three letters, filling less -->
<span class="x-avatar" data-size="5">
  <span class="x-avatar-fallback" data-size="1">WBX</span>
</span>

There is no character-count selector in CSS, so the ratio is the author's to pick — but the count is known wherever the initials are produced, so it usually falls out of the same expression. Override --avatar-fallback-scale directly for a ratio between the three.

The computed size is floored at --font-size-1, so the smallest avatars stay legible rather than following the ratio down to unreadable text. One consequence: three letters do not fit a data-size="1" avatar at any legible size — 12px text needs roughly 28px of width in a 24px box — and get clipped. Use data-size="3" or larger for three-letter fallbacks, or an icon.

Open in Storybook with controls ↗

An <svg> in the fallback ignores the type scale and is sized to 60% of the avatar box instead, so a generic person glyph works at every data-size:

<span class="x-avatar" data-size="4">
  <span class="x-avatar-fallback">
    <svg viewBox="0 0 64 64" aria-hidden="true" style="fill: currentColor">
      <path d="…" />
    </svg>
  </span>
</span>
Open in Storybook with controls ↗

Fallback behavior

Both layers are absolutely positioned; .x-avatar-image sits above the fallback via z-index. It hides itself whenever it has nothing to paint:

.x-avatar-image:where(:not([src]), [src=""], [data-status="error"]) {
  display: none;
}

So there are three cases, and only one of them needs script:

CaseHandling
No src, or src=""pure CSS — no JS, no error event
Load failed (404, CORS, decode)the error handler sets data-status="error"
Still loadingnothing to do — an in-flight <img> paints nothing, so the fallback already shows

A broken <img> that stays visible is not invisible — every engine paints its own artifact over the fallback:

EngineBroken <img alt=""> renders
Chromebroken-image glyph
Safaribroken-image glyph
Firefox1px border ring
Open in Storybook with controls ↗

No CSS suppresses all three, so flag the image on error. How, depends on who owns the DOM:

<!-- Static HTML — shortest thing that works -->
<img class="x-avatar-image" src="/wael.jpg" alt="" onerror="this.remove()" />
// Vanilla + strict CSP — one delegated listener, no inline handler.
// `error` does not bubble, so capture it.
document.addEventListener(
  "error",
  (event) => {
    const img = event.target;
    if (img instanceof HTMLImageElement && img.matches(".x-avatar-image")) {
      img.dataset.status = "error";
    }
  },
  true,
);
// React / Vue / Svelte — render the attribute, never mutate the node.
// `this.remove()` leaves the vdom believing the <img> is still mounted, and
// the next re-render puts it back.
const [failed, setFailed] = useState(false);

<img
  class="x-avatar-image"
  src={src}
  alt=""
  data-status={failed ? "error" : undefined}
  onError={() => setFailed(true)}
/>;

Setting the attribute is preferable to removing the element in every context that re-renders: it is non-destructive, so a retry or a changed src can clear it.

Open in Storybook with controls ↗

Keep alt="" whichever tier you use — the initials are decorative and the name belongs on the root, see Accessibility.

Open in Storybook with controls ↗

Sizing

data-sizesize
1--space-5
2--space-6
3--space-7
4--space-8
5--space-9
680px
796px
8128px
9160px

Sizes 6–9 are pixel values multiplied by --scaling, so data-scaling on an ancestor still applies. Fallback type and icon size derive from the box, so they follow automatically — only the box size and corner radius are enumerated here.

Open in Storybook with controls ↗

Variants

  • soft (default) — tinted fallback background, accent text.
  • solid — accent fill, high-contrast text.

The variant only paints the fallback; an avatar showing its image looks identical either way.

Radius

Corner radius scales with data-size and follows data-radius on any ancestor. data-radius="pill" makes avatars fully circular:

<div data-radius="pill">
  <span class="x-avatar" data-size="4">
    <span class="x-avatar-fallback">WB</span>
  </span>
</div>

Set it on the avatar itself for a one-off circle. Available values: none, small, medium, large, pill.

Theming

Switch the color on a single avatar with data-accent="<color>", or set it on any ancestor to theme the whole subtree. Deriving the accent from the user's name gives stable per-user colors:

<span class="x-avatar" data-accent="amber" data-variant="soft">
  <span class="x-avatar-fallback">AB</span>
</span>

Light/dark flips automatically via the .dark class on a parent — see Light & dark.

Accessibility

.x-avatar ships no role — it's a visual class. alt="" keeps the image out of the accessibility tree, so the accessible name belongs on the root when the avatar carries information:

PatternMarkup
Decorative — name appears next to the avatar<span class="x-avatar">…</span>
Standalone — avatar is the only identifier<span class="x-avatar" role="img" aria-label="Wael Bettayeb">…</span>
Link or button<a class="unset x-avatar" href="/u/wael" aria-label="Wael Bettayeb">…</a>

Add unset when rendering the avatar as <a> or <button> to neutralise native styling. Initials in the fallback are decorative in every case — they duplicate a name that assistive tech should read in full.