Skip to content

Avatar

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.

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.

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>

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

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.

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

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.

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.