Skip to content

[Avatar] Derive Image loading status from the rendered element (make render work with next/image) #5529

Description

@sss-Mihail-sss

Summary

Let Avatar.Image derive its loading status from the actually rendered element (its onLoad / onError) instead of always preloading the raw src through an internal new window.Image(). Equivalently: an opt-out for the internal preload, or an externally controlled loadingStatus.

Today AvatarImage calls useImageLoadingStatus(src, elementProps) unconditionally, and that hook does:

const image = new window.Image();
// …
image.src = src; // fetches the RAW src to determine loaded/error

Because of this, render only swaps the visual element — the status is still derived from a side-channel fetch of the raw src. So <Avatar.Image render={<NextImage … />} /> (or any custom image component / optimizer / CDN loader) does not work as one would expect:

  1. Double fetch — the raw original is downloaded by new Image() for status, and the optimized/loader version is downloaded by the rendered element.
  2. Serialized — the element is mounted only when imageLoadingStatus === 'loaded' (enabled: mounted, if (!mounted) return null), so the rendered element only starts loading after the raw preload finishes.

There's no way to keep the Fallback coordination while avoiding the raw preload: dropping src makes the hook report 'error' immediately, so the image never shows.

Proposed API (either would solve it)

  • An opt-out that reads status from the rendered element's own onLoad/onError instead of new Image(), e.g. <Avatar.Image nativeLoadingStatus={false} /> (name TBD); or
  • A controlled mode: accept loadingStatus (+ keep onLoadingStatusChange) so the consumer can drive it from the rendered element.

The Avatar.RootAvatar.Fallback coordination and data-*/transition attributes would stay the same — only the source of the status changes.

Examples in other libraries

Not aware of a headless avatar that solves this cleanly — Radix UI's Avatar.Image (which Base UI's Avatar mirrors) has the same internal new Image() preload and the same limitation. The natural status source already exists on the rendered element: next/image (and a plain <img>) expose onLoad/onError, which is exactly what a consumer hand-rolls today to get single, optimized loading:

// what we do instead of Avatar.Image, to avoid the raw preload:
const [loaded, setLoaded] = useState(false);
<Image src={src} fill sizes={sizes} onLoad={() => setLoaded(true)} onError={() => setLoaded(false)}  />
// Fallback shown while !loaded

Motivation

We serve avatars/logos through an image route and use next/image to resize to the actual display size (32–144 px) from a single 1024 px source. We'd like Base UI's Avatar for the Root/Image/Fallback status coordination, rendering next/image via render.

The blocker is the internal raw preload: it downloads the full 1024 px original for every avatar regardless of render (defeating optimization and wasting bandwidth, e.g. long lists of small avatars), and it delays the optimized fetch until the raw one completes. Because the preload is the status mechanism, render alone can't fix it.

Letting the status come from the rendered element would make Avatar.Image compose with next/image, custom loaders, and CDNs — one optimized request, no raw-original download — while keeping the ergonomic Root/Image/Fallback API. This is what forces consumers to reimplement Avatar.Image by hand today.

Metadata

Metadata

Assignees

No one assigned

    Labels

    component: avatarChanges related to the avatar component.type: new featureExpand the scope of the product to solve a new problem.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions