Skip to content

feat(svelte-ds-app-launchpad): Add UserAvatar component#606

Open
steciuk wants to merge 4 commits intomainfrom
feat/upstream-user-avatar
Open

feat(svelte-ds-app-launchpad): Add UserAvatar component#606
steciuk wants to merge 4 commits intomainfrom
feat/upstream-user-avatar

Conversation

@steciuk
Copy link
Copy Markdown
Contributor

@steciuk steciuk commented Apr 8, 2026

Done

  • Add UserAvatar component that displays an avatar image when available, falls back to user initials if userName is provided, and a generic icon placeholder otherwise.
  • No-JS fallback: when the image fails to load without JS, initials are shown via CSS ::after on the <img> if userName is provided and a ? otherwise.

QA

  • bun run check && bun run test
  • verify Components/UserAvatar in Storybook
    • check image renders when userAvatarUrl is provided
    • check initials fallback when no image URL is given or image fails to load
    • check icon placeholder renders when neither image nor name is provided

PR readiness check

  • PR should have one of the following labels:
    • Feature 🎁, Breaking Change 💣, Bug 🐛, Documentation 📝, Maintenance 🔨.
  • PR title follows the Conventional Commits format.
  • The code follows the appropriate code standards
  • All packages define the required scripts in package.json:
    • All packages: check, check:fix, and test.
    • Packages with build steps: build to build the package for development or distribution, build:all to build all artifacts. See CONTRIBUTING.md for details.
  • If this PR introduces a new package: first-time publish has been done manually from inside the package directory using npm publish --access public (first-time publishing is not automated). Run bun run publish:status from the repo root to verify.

Screenshots

image image

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new UserAvatar component to the Svelte Launchpad design system, providing an avatar image with fallbacks to user initials or a generic icon, along with Storybook coverage and tests.

Changes:

  • Introduce UserAvatar Svelte component with size variants and image/initials/icon fallback logic.
  • Add getInitials utility with unit tests.
  • Add Storybook stories plus browser + SSR tests, and export the component from the components barrel.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/svelte/ds-app-launchpad/src/lib/components/UserAvatar/UserAvatar.svelte Implements the new UserAvatar component rendering logic and fallbacks
packages/svelte/ds-app-launchpad/src/lib/components/UserAvatar/types.ts Defines public props/types for UserAvatar
packages/svelte/ds-app-launchpad/src/lib/components/UserAvatar/styles.css Adds styling for avatar, sizes, and fallback presentation
packages/svelte/ds-app-launchpad/src/lib/components/UserAvatar/index.ts Exports UserAvatar component and types
packages/svelte/ds-app-launchpad/src/lib/components/UserAvatar/UserAvatar.stories.svelte Adds Storybook stories for typical and edge-case states
packages/svelte/ds-app-launchpad/src/lib/components/UserAvatar/UserAvatar.svelte.test.ts Adds browser-level component tests
packages/svelte/ds-app-launchpad/src/lib/components/UserAvatar/UserAvatar.ssr.test.ts Adds SSR rendering tests
packages/svelte/ds-app-launchpad/src/lib/components/UserAvatar/utils/getInitials.ts Implements initials extraction helper
packages/svelte/ds-app-launchpad/src/lib/components/UserAvatar/utils/getInitials.test.ts Adds unit tests for initials extraction helper
packages/svelte/ds-app-launchpad/src/lib/components/UserAvatar/utils/index.ts Re-exports getInitials utility
packages/svelte/ds-app-launchpad/src/lib/components/index.ts Re-exports UserAvatar from the main components barrel

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/svelte/ds-app-launchpad/src/lib/components/UserAvatar/UserAvatar.svelte Outdated
Comment thread packages/svelte/ds-app-launchpad/src/lib/components/UserAvatar/UserAvatar.svelte Outdated
Comment thread packages/svelte/ds-app-launchpad/src/lib/components/UserAvatar/UserAvatar.svelte Outdated
@steciuk steciuk force-pushed the feat/upstream-user-avatar branch from d538d60 to 8264b48 Compare April 9, 2026 07:50
Comment thread packages/svelte/ds-app-launchpad/src/lib/components/UserAvatar/UserAvatar.svelte Outdated
};

export interface UserAvatarProps
extends Omit<HTMLAttributes<HTMLElement>, "children">,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would we like callers to be able to pass image props here? In that case, it would be nice to extend HTMLAttributes<HTMLImageElement>, but this is complicated because the root element of this component is a div in some cases.

Maybe something like this would work (haven't tried this):

interface BaseAvatarProps {
  size?: "small" | "medium" | "large";
  class?: string;
}

interface AvatarWithImage extends BaseAvatarProps, Omit<HTMLImgAttributes, "size" | "src"> {
  userAvatarUrl: string;
  userName?: string;
}

interface AvatarWithoutImage extends BaseAvatarProps, Omit<HTMLAttributes<HTMLDivElement>, "size"> {
  userAvatarUrl?: never; // This is the magic "discriminated" part
  userName?: string;
}

export type UserAvatarProps = AvatarWithImage | AvatarWithoutImage;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This approach would be problematic here, because the root element type not only depends on the presence of userAvatarUrl (where this would be ok to use), but also on the possibility of an image loading error when the img is replaced with the div.

What we could do is to manually pick a subset of the most commonly used img attributes, destructure them from rest in $props() definition and only forward to the img element. If you think that's the move, what attributes should we choose? alt, decoding, fetchpriority, loading? Do you have any other ideas on how to handle that?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

My gut is to restructure things somewhat so that we actually have different components based on what the root element is to simplify the types, so we don't need to own the maintenance of picking "commonly used" attributes.

  • UserAvatar that controls the logic for determining which specific component to use based on loading state, etc
    • UserAvatarImage that is always an image
    • UserAvatarPlaceholder or other name that is the div used in error/loading

Then at the UserAvatar level you could expose two sets of props, imageProps, and placeholderProps, and spread those onto the respective elements when they are rendered. Do you think this is any nicer?

Copy link
Copy Markdown
Contributor Author

@steciuk steciuk Apr 16, 2026

Choose a reason for hiding this comment

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

My main concern is that we'd be trading our

maintenance of picking "commonly used" attributes

for a more complex consumer API - one that doesn't occur in any of our other components so far.

For example, if a consumer wants to apply a class and an aria-label, they would be forced to specify those props twice:

<UserAvatar
  userName="John"
  imageAttributes={{
    src: "https://assets.ubuntu.com/v1/fca94c45-snap+icon.png",
    alt: "User Avatar",
    
    class: "my-class",
    "aria-label": "User Avatar",
  }}
  placeholderAttributes={{
    class: "my-class",
    "aria-label": "User Avatar",
  }}
/>

If we were to eventually separate things further into imageProps, initialsProps, and iconProps, they might even have to define them three times! And to be frank, I am not a fan of this direction.

It also forces us into other tricky decisions. For example - regarding attachments. Should each root element have its own way of passing an attachment to keep things consistent but force the user into questionable ergonomics of relying on createAttachmentKey?

<UserAvatar
  imageAttributes={{
    [createAttachmentKey()]: (el) => {
      // ...
    },
  }}
  placeholderAttributes={{
    [createAttachmentKey()]: (el) => {
      // ...
    },
  }}
/>

Or, do we expose one shared attachment at the root level, breaking the pattern of grouping things by element?

<UserAvatar
  imageAttributes={{
    //...
  }}
  placeholderAttributes={{
    //...
  }}
  {@attach (el) => {
    // ...
  }}
/>

Personally, I value keeping the consumer API simple, ergonomic, and consistent with the rest of our components, even if it means we have to take on a bit of extra maintenance under the hood.

If our main goal is to avoid making a decision on "which img-specific attributes to choose", I would prefer to just manually expose all that the standard currently defines:

  • alt
  • crossorigin
  • decoding
  • fetchpriority
  • height
  • ismap
  • loading
  • referrerpolicy
  • sizes
  • src
  • srcset
  • usemap
  • width

There really aren't that many of them, and I wouldn't expect the list of standard img attributes to change very often anyway.

Let me know what you think.

Copy link
Copy Markdown
Contributor Author

@steciuk steciuk Apr 16, 2026

Choose a reason for hiding this comment

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

I came up with another, somewhat "hybrid" option that I think is the best middle ground between the two approaches. The idea is to keep all attributes that are shared between the root elements as flat component props and allow for imageAttributes that'd contain only the ones that are img-specific.

import type { HTMLAttributes, HTMLImgAttributes } from "svelte/elements";

type ImageOnlyAttributes = Omit<
  HTMLImgAttributes,
  | keyof HTMLAttributes<HTMLElement>
  | "bind:naturalWidth"
  | "bind:naturalHeight"
  | "children"
>;

export interface UserAvatarProps extends Omit<
  HTMLAttributes<HTMLElement>,
  "children"
> {
  userName?: string;
  size?: "small" | "medium" | "large";
  imageAttributes?: ImageOnlyAttributes;
}

And then:

<script lang="ts">
  const {
    class: classProp,
    userName: userNameProp,
    size = "medium",
    imageAttributes,
    ...rest
  }: UserAvatarProps = $props();
  
  // ...
</script>

{#if imageAttributes?.src && !imageError}
  <img
    class={className}
    title={userName || undefined}
    data-initials={userInitials}
    onerror={() => (imageError = true)}
    {...imageAttributes}
    {...rest}
  />
{:else if userName}
  <abbr class={className} title={userName} {...rest}>
    {userInitials}
  </abbr>
{:else}
  <div class={className} {...rest}>
    <UserIcon />
  </div>
{/if}

This raises a different question, related to your other comment. If we move forward with this approach, do we:

  1. Keep src within imageAttributes. This splits user-related data across different levels (the userName root property vs. imageAttributes.src).
  2. Remove src from imageAttributes and move it to the root as userImageUrl, placing it directly alongside userName.
  3. Remove src from imageAttributes and create a root-level user object containing both name and url.

I personally lean toward Option 2, as it surfaces the most important properties for better visibility.

Please let me know what your thoughts are.

Comment on lines +8 to +9
/** The URL of the user's avatar image */
userAvatarUrl?: string;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why can't we just use the native src for this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We decided to use userAvatarUrl to keep the UserOptions type reusable and decoupled from the specifics of the img element (with all its properties relating to a user "user..."). You can see an example of its use in the Timeline.Event component.

We could stick to src here and e.g., map userAvatarUrl to it within the Timeline, but that would create an API inconsistency.

Please let me know which approach you prefer :)

Copy link
Copy Markdown
Member

@jmuzina jmuzina Apr 15, 2026

Choose a reason for hiding this comment

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

We could stick to src here and e.g., map userAvatarUrl to it within the Timeline, but that would create an API inconsistency.

Is that really an "inconsistency", or is it a more honest modeling of how the information's meaning is different from component to component? At the level of the Timeline, it makes sense to scope these props to "user" because it's a different information space than the component in question. In the user avatar, though, it feels a bit redundant to repeat user... in these props (the component name already tells us this is related to a user).

We should be sure this is a sound approach though, I don't want to make the API or implementation cumbersome due to this. Do you have any opinions here @nathanclairmonte @jademathre-canonical @goulinkh @alvaromateo ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I like this framing, and I'm happy with either option. Let's see what other folks think!

Comment on lines +8 to +9
/** The URL of the user's avatar image */
userAvatarUrl?: string;
Copy link
Copy Markdown
Member

@jmuzina jmuzina Apr 15, 2026

Choose a reason for hiding this comment

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

We could stick to src here and e.g., map userAvatarUrl to it within the Timeline, but that would create an API inconsistency.

Is that really an "inconsistency", or is it a more honest modeling of how the information's meaning is different from component to component? At the level of the Timeline, it makes sense to scope these props to "user" because it's a different information space than the component in question. In the user avatar, though, it feels a bit redundant to repeat user... in these props (the component name already tells us this is related to a user).

We should be sure this is a sound approach though, I don't want to make the API or implementation cumbersome due to this. Do you have any opinions here @nathanclairmonte @jademathre-canonical @goulinkh @alvaromateo ?

};

export interface UserAvatarProps
extends Omit<HTMLAttributes<HTMLElement>, "children">,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

My gut is to restructure things somewhat so that we actually have different components based on what the root element is to simplify the types, so we don't need to own the maintenance of picking "commonly used" attributes.

  • UserAvatar that controls the logic for determining which specific component to use based on loading state, etc
    • UserAvatarImage that is always an image
    • UserAvatarPlaceholder or other name that is the div used in error/loading

Then at the UserAvatar level you could expose two sets of props, imageProps, and placeholderProps, and spread those onto the respective elements when they are rendered. Do you think this is any nicer?

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants