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.
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>| Attribute | Values |
|---|---|
data-variant | soft (default), solid |
data-size | 1–9 (default 3) — on .x-avatar, the box size |
data-size | 1–3 — on .x-avatar-fallback, the type ratio |
data-accent | any imported color scale (iris, blue, amber, …) |
data-high-contrast | modifier — boosts contrast against the page background |
data-status | error — on .x-avatar-image, hides it so the fallback shows |
| Class | Role |
|---|---|
.x-avatar | root — owns size, radius, and clipping |
.x-avatar-fallback | bottom layer — centered initials |
.x-avatar-image | top 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-size | font-size | for |
|---|---|---|
1 | 30% of the box | three letters or more |
2 (default) | 40% of the box | two initials |
3 | 50% of the box | a 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:
| Case | Handling |
|---|---|
No src, or src="" | pure CSS — no JS, no error event |
| Load failed (404, CORS, decode) | the error handler sets data-status="error" |
| Still loading | nothing 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:
| Engine | Broken <img alt=""> renders |
|---|---|
| Chrome | broken-image glyph |
| Safari | broken-image glyph |
| Firefox | 1px 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-size | size |
|---|---|
1 | --space-5 |
2 | --space-6 |
3 | --space-7 |
4 | --space-8 |
5 | --space-9 |
6 | 80px |
7 | 96px |
8 | 128px |
9 | 160px |
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:
| Pattern | Markup |
|---|---|
| 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.