Avatar
stimeo--avatar
Shows a user image and swaps to a fallback when it fails to load.
The stimeo--avatar controller has no dedicated APG pattern; it follows the non-text-content practice (WCAG 1.1.1). It watches the inner <img> for load and error and swaps to the author-provided fallback when the image fails or no src is given, exposing the loading / loaded / error phase on data-state. The accessible name lives on the container (role="img" + aria-label) while the <img> and the fallback are aria-hidden, so assistive tech reads the name once regardless of which side is visible. Behavior only — shape, size, and colour are owned by this Playground; initials or colour generation are out of scope.
<%# Markup for the avatar demo.
The accessible name is owned solely by the container (role="img" + aria-label);
the inner <img> and fallback use aria-hidden="true" to avoid double announcement.
The library only detects load/error and toggles display (data-state); shape,
color, and size are drawn by demo.css. %>
<div class="avatar-demo">
<%# 1. An avatar that loads successfully. The src is a self-contained data-URI SVG that
draws a person silhouette (a picture, not letters) so the "loaded image" state reads
clearly different from the initials fallback below. %>
<figure class="avatar-demo__item">
<span
class="avatar"
data-controller="stimeo--avatar"
role="img"
aria-label="<%= t("components.avatar.demo.name_ok") %>"
data-stimeo--avatar-src-value="data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='96'%20height='96'%3E%3Cdefs%3E%3ClinearGradient%20id='g'%20x1='0'%20y1='0'%20x2='0'%20y2='1'%3E%3Cstop%20offset='0'%20stop-color='%236366f1'/%3E%3Cstop%20offset='1'%20stop-color='%234338ca'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect%20width='96'%20height='96'%20fill='url(%23g)'/%3E%3Ccircle%20cx='48'%20cy='38'%20r='17'%20fill='%23eef2ff'/%3E%3Cpath%20d='M20%2084%20a28%2028%200%200%201%2056%200%20Z'%20fill='%23eef2ff'/%3E%3C/svg%3E">
<img
class="avatar__image"
alt=""
aria-hidden="true"
data-stimeo--avatar-target="image"
data-action="load->stimeo--avatar#onLoad error->stimeo--avatar#onError">
<span
class="avatar__fallback"
aria-hidden="true"
hidden
data-stimeo--avatar-target="fallback"
>AU</span>
</span>
<figcaption><%= t("components.avatar.demo.caption_ok") %></figcaption>
</figure>
<%# 2. An avatar that fails to load (nonexistent path → fallback initials). The browser
logs an expected 404 for the missing image — that is the point: it triggers the error
→ fallback path the library handles. %>
<figure class="avatar-demo__item">
<span
class="avatar"
data-controller="stimeo--avatar"
role="img"
aria-label="<%= t("components.avatar.demo.name_fallback") %>"
data-stimeo--avatar-src-value="/this-image-does-not-exist.jpg">
<img
class="avatar__image"
alt=""
aria-hidden="true"
data-stimeo--avatar-target="image"
data-action="load->stimeo--avatar#onLoad error->stimeo--avatar#onError">
<span
class="avatar__fallback"
aria-hidden="true"
hidden
data-stimeo--avatar-target="fallback"
>JD</span>
</span>
<figcaption><%= t("components.avatar.demo.caption_fallback") %></figcaption>
</figure>
</div>
/* Presentation CSS for avatar. The library only detects load/error and toggles
data-state / hidden, so shape, color, size, and the fallback look are all here. */
.avatar-demo {
display: flex;
gap: 2rem;
flex-wrap: wrap;
}
.avatar-demo__item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
margin: 0;
/* Cap the column width so a longer caption wraps under the avatar (centered)
instead of stretching the item and breaking the row layout. */
max-width: 12rem;
font-size: 0.85rem;
color: var(--color-text-muted);
text-align: center;
}
.avatar-demo__item figcaption {
text-wrap: balance;
}
/* The container owns the circular clip; the inner image / fallback are layered inside. */
.avatar {
position: relative;
display: inline-grid;
place-items: center;
width: 96px;
height: 96px;
border-radius: 50%;
overflow: hidden;
background: var(--surface-subtle);
box-shadow: 0 0 0 2px var(--surface-subtle), 0 1px 4px rgba(15, 23, 42, 0.2);
}
.avatar__image {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Fallback initials. Visible only once the hidden attribute is removed. */
.avatar__fallback {
font-size: 2rem;
font-weight: 600;
color: var(--white);
background: var(--slate-500);
width: 100%;
height: 100%;
display: grid;
place-items: center;
}
/* data-state is set by the library. Example: a subtle pulse while loading. */
.avatar[data-state="loading"] {
animation: avatar-pulse 1.2s ease-in-out infinite;
}
@keyframes avatar-pulse {
50% {
opacity: 0.6;
}
}
@media (prefers-reduced-motion: reduce) {
.avatar[data-state="loading"] {
animation: none;
}
}
This demo needs no consumer-side JS (the controller handles the behavior).
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--avatar"
Targets
| Name | Description | Attribute |
|---|---|---|
image
required
|
The <img> whose load/error is watched and which is shown on success. |
data-stimeo--avatar-target="image" |
fallback
|
The author-provided fallback element shown when the image fails or no src is given. | data-stimeo--avatar-target="fallback" |
Values
| Name | Description | Attribute |
|---|---|---|
src
|
The image source applied to the <img>; when set it is the source of truth (defaults to empty, honoring the markup's own src). |
data-stimeo--avatar-src-value |
Actions
| Name | Description | Action |
|---|---|---|
onError
|
Swaps to the fallback when the image fails to load and emits the error event. | stimeo--avatar#onError |
onLoad
|
Reveals the image once it has loaded successfully. | stimeo--avatar#onLoad |
Events
| Name | Description | Event |
|---|---|---|
error
|
Dispatched when the image fails to load; detail carries the attempted src. | stimeo--avatar:error |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
data-state |
Container | "loading" / "loaded" / "error". |
hidden |
Image / Fallback | Only the visible side is shown. |