Show an image with a fallback while it loads or if it fails.

Use an Avatar for a person or a team, like in a comment, a member list or an account menu. It shows initials or an icon until the image loads, and keeps showing them if the image fails.

import "@ui-sdk/web/avatar"

const person = {
  name: "shadcn",
  src: "https://github.com/shadcn.png",
}

function mount(root: HTMLElement) {
  root.innerHTML = `
    <div data-slot="row">
      <ui-avatar>
        <img src="${person.src}" alt="${person.name}" />
        <ui-avatar-fallback><span aria-hidden="true">CN</span><span data-slot="visually-hidden">shadcn</span></ui-avatar-fallback>
      </ui-avatar>
    </div>
  `
}

export { mount }
The image replaces the initials once it loads. A screen reader can read the person's name in either state.

Installation

npm install @ui-sdk/web @ui-sdk/core

Importing the component defines its elements. On the server the import does nothing.

import "@ui-sdk/web/avatar"

Composition

Use the following composition to build a ui-avatar:

ui-avatar
├── img
└── ui-avatar-fallback
  • ui-avatar shares the image's loading state with its parts.
  • A direct-child img supplies the image. There is no image custom element.
  • ui-avatar-fallback shows when no image is loaded.

The image is a real img, including in server markup. The browser can start fetching it before the elements upgrade. The fallback stays visible until the image loads; if loading fails, the fallback remains.

Usage

Import the parts and put them together:

<script type="module">
  import "@ui-sdk/web/avatar"
</script>

<ui-avatar>
  <img src="/ada.png" alt="Ada Lovelace" />
  <ui-avatar-fallback>AL</ui-avatar-fallback>
</ui-avatar>

Build Your Own

@ui-sdk/web/avatar comes with ready-made ui-avatar elements. If you'd like to build your own, with your own element names, start from the avatar primitive in @ui-sdk/core:

user-photo-elements.ts
import { avatar } from "@ui-sdk/core/avatar"
import { createParts } from "@ui-sdk/web"

createParts(avatar, {
  root: {
    tag: "user-photo",
    children: { image: (el) => el.querySelector(":scope > img") },
    optionalChildren: ["image"],
  },
  fallback: "user-photo-initials",
})
<user-photo>
  <img src="https://github.com/shadcn.png" alt="shadcn" />
  <user-photo-initials>CN</user-photo-initials>
</user-photo>

These are the same parts the ready-made elements are made of, with your names. See Rendering and Custom Rendering for more.

Fallback Delay

Set delay on the fallback to wait before showing it. This avoids flashing initials when an image loads quickly. Without a delay, the fallback appears immediately, including on the server.

<ui-avatar>
  <img src="https://github.com/shadcn.png" alt="shadcn" />
  <ui-avatar-fallback delay="600">CN</ui-avatar-fallback>
</ui-avatar>

Image Attributes

Pass image attributes such as srcset, crossorigin, and referrerpolicy to the image. They work on the same img the browser loads. For avatars farther down a page, use loading="lazy".

<ui-avatar>
  <img
    src="https://github.com/shadcn.png"
    srcset="
      https://github.com/shadcn.png?size=80  1x,
      https://github.com/shadcn.png?size=160 2x
    "
    alt="shadcn"
    crossorigin="anonymous"
    referrerpolicy="no-referrer"
    loading="lazy"
  />
  <ui-avatar-fallback>CN</ui-avatar-fallback>
</ui-avatar>

Interactive Avatar

Use a button when clicking an avatar performs an action. Give it an accessible name that stays available while the image loads.

<button type="button" aria-label="shadcn">
  <ui-avatar>
    <img src="https://github.com/shadcn.png" alt="" />
    <ui-avatar-fallback aria-hidden="true">CN</ui-avatar-fallback>
  </ui-avatar>
</button>

The img has to stay a direct child of ui-avatar, so put the avatar inside the button instead of binding the button with as-child.

Loading Status

Listen for loadingstatuschange on ui-avatar. The event detail contains the new status in value and the change details beside it. The possible statuses are loading, loaded, error, and idle.

const avatar = document.querySelector("ui-avatar")!

avatar.addEventListener("loadingstatuschange", (event) => {
  if (event.detail.value === "error") reportBrokenImage(event.detail.reason)
})

When you replace a loaded image, the browser keeps it visible while the next source loads. A failed replacement shows the fallback. The change details explain why the status changed; see Reasons.

Custom Rendering

Use Parts and Composition to choose the elements parts bind. Every part shows the loading status, and ui-avatar reports each change, so the fallback can say something different when the image fails:

const avatar = document.querySelector("ui-avatar")!
const fallback = avatar.querySelector("ui-avatar-fallback")!

avatar.addEventListener("loadingstatuschange", (event) => {
  fallback.textContent = event.detail.value === "error" ? "?" : user.initials
})

Styling

Set the root's size and let the image and fallback fill it. The image manages its own hidden styles until it loads; keep those styles intact when changing its element.

Each part has a data-slot, such as avatar-fallback, and shows the loading status through the attributes listed under Data Attributes.

Data Attributes

<ui-avatar>

[data-slot="avatar"]

AttributeWhat it means
data-loading-statusidleloadingloadederrorWhere the image is: no image yet, loading, loaded, or failed.

<img>

[data-slot="avatar-image"]

AttributeWhat it means
data-loading-statusidleloadingloadederrorWhere the image is: no image yet, loading, loaded, or failed.

<ui-avatar-fallback>

[data-slot="avatar-fallback"]

AttributeWhat it means
data-loading-statusidleloadingloadederrorWhere the image is: no image yet, loading, loaded, or failed.

API Reference

Props are attributes in kebab case, like delay on <ui-avatar-fallback>. Callbacks are DOM events.

Avatar

loadingstatuschange event
Runs when the image's loading status changes: "loading" when it gets a source, "loaded", "error", and "idle" when it leaves the page. details.reason says why, and details.event is the image's load or error event, or undefined when the status was known at once, like for a cached image.
debug?boolean
Logs every state change with its reason to the console, in development builds.

AvatarImage

The image. A real img, a direct child of ui-avatar, so the browser can fetch it before the elements upgrade, with its own attributes, such as srcset, crossorigin and referrerpolicy.

AvatarFallback

delay?number
How long to wait, in milliseconds, before showing the fallback, so a fast image appears without the fallback flashing first. Without one, the fallback shows at once, server markup included.

State

The same state shows as data attributes on each element.

loadingStatusAvatarLoadingStatus
Where the image is: "idle", "loading", "loaded" or "error".

Reasons

event.detail.reason says why the status changed.

image-loadEvent | undefined
An image loaded. No event when it was already in the browser's cache.
image-errorEvent | undefined
An image failed to load, or has no source. No event when that was known at once.
src-changenone
An image got a new source, or left the page, so its status starts over.

Events

Events fire on their own element and don't bubble, with the status in event.detail.value and the reason and event beside it.

MemberOnDescription
loadingstatuschange eventui-avatardetail: { value, reason, event }, when the status changes
delay attributeui-avatar-fallbackHow long to wait before showing the fallback, in milliseconds
debug attributeui-avatarLogs every status change with its reason

Accessibility

Give the image meaningful alt text. Include the person's name in the fallback too, using visually hidden text if you only want to show initials.

When a visible name beside the avatar already identifies the person, use an empty alt and hide decorative fallback content from screen readers.

  • An image that loads replaces the fallback.
  • An image that fails keeps the fallback.
  • An avatar without an image shows its fallback.
  • A fallback with a delay waits before showing.
  • The image's loading status is reported.
  • Removing the image brings the fallback back.
  • A new source shows its image once it loads.
  • The image is fetched with its own referrer policy.
  • A cached image doesn't flash the fallback when the avatar mounts again.
  • A new source keeps showing the previous image until it loads.
  • The image is fetched once, even when it can't be cached.

Runs in React, Vue and web components.