Skip to content

avatar: Add mode prop#3943

Open
chaance wants to merge 2 commits into
mainfrom
chance/avatar-enhancements
Open

avatar: Add mode prop#3943
chaance wants to merge 2 commits into
mainfrom
chance/avatar-enhancements

Conversation

@chaance

@chaance chaance commented Jun 7, 2026

Copy link
Copy Markdown
Member

This PR introduces a new mode prop for Avatar.Image for more control over loading state and rendering.

  • mode="default" (default): Matches the existing behavior by rendering an img element conditionally based on the loading state of an Image object constructed after hydration. This ensures backwards compatibility.

  • mode="native": Renders an img element unconditionally using its event handlers to update loading state. In native mode, you can use CSS to target the image element and style it based on its loading state. Closes [Avatar] Avatar.Image doesn't implement lazy loading #2512 since the lazy attribute would dictate when the loading handler is called.

    <Avatar.Root className="image-root">
      <Avatar.Image
        mode="native"
        className="image"
        alt="John Smith"
        src="./avatar.png"
      />
      <Avatar.Fallback className="fallback">
        <AvatarIcon />
      </Avatar.Fallback>
    </Avatar.Root>
    .image-root {
      position: relative;
    }
    .image[data-radix-avatar-loading-status]:not(
        [data-radix-avatar-loading-status="loaded"]
      ) {
      /* hide the element visually until it's loaded to reveal the fallback */
      position: absolute;
      inset: 0;
      opacity: 0;
    }
  • mode="custom": Allows for more control over the image rendering. The render prop must be provided to render the image. This mode is useful when you want to use a custom image component, such as framework-specific image components or a design-system implementation that uses non-standard props. Closes [Avatar] Allow Avatar.Image to be supplied a component to render the image #2230

    <Avatar.Image
      mode="custom"
      className="image"
      alt="John Smith"
      src="./avatar.png"
      render={({ props, ref, context }) => (
        <CustomImage
          {...props}
          ref={ref}
          data-loading-status={context.loadingStatus}
          onLoadingComplete={({ naturalWidth, naturalHeight }) => {
            context.onLoadingStatusChange("loaded");
          }}
          onError={() => {
            context.onLoadingStatusChange("error");
          }}
        />
      )}
    />

@changeset-bot

changeset-bot Bot commented Jun 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f75cedb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@radix-ui/react-avatar Minor
radix-ui Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chaance chaance requested a review from dcwither June 7, 2026 22:35

@thompsongl thompsongl left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. The mode enum makes sense to me and the implementation appears to provide the necessary & requested styling hooks

forwardedRef,
React.useCallback(
(node: HTMLImageElement | null) => {
if (!src) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default mode sets status error + fires onLoadingStatusChange when src is missing; native stays idle and never fires. Should align them or document the difference.

context.setImageLoadingStatus('error'),
)}
onLoad={composeEventHandlers(imageProps.onLoad, (event) => {
if (!src) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if (!src) return; guard and comment are copied from the ref callback. onLoad can only fire once the image has a src and loads, so the branch looks unreachable here, and the comment (about not updating from error) describes the ref-callback case rather than this one.

}, [imageLoadingStatus, handleLoadingStatusChange]);

return (
<Primitive.img

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always renders, which can lead to duplicate information between this and fallback on non-loaded states - should this be aria-hidden on non-loaded states?

.image-root {
position: relative;
}
.image[data-radix-avatar-loading-status]:not(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data-radix-avatar-loading-status diverges from the data-state convention for consumer-facing state elsewhere (Progress exposes its loading lifecycle that way too) — I'm guessing this is an intentional direction change?

Also these are necessary to avoid duplicate UI, so it should maybe be called out more loudly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Avatar] Avatar.Image doesn't implement lazy loading [Avatar] Allow Avatar.Image to be supplied a component to render the image

3 participants