From cb842868f5cbc3216239cbd5ec3dea79922fbdd4 Mon Sep 17 00:00:00 2001 From: Naveed Sajjad <88137903+naveedsajjdawan@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:43:21 +0500 Subject: [PATCH 1/9] feat(Avatar): support text placeholders --- packages/react/src/Avatar/Avatar.tsx | 66 ++++++++++++++++++---------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/packages/react/src/Avatar/Avatar.tsx b/packages/react/src/Avatar/Avatar.tsx index b9e501a8412..a3324234d10 100644 --- a/packages/react/src/Avatar/Avatar.tsx +++ b/packages/react/src/Avatar/Avatar.tsx @@ -11,16 +11,18 @@ export type AvatarProps = { size?: number | ResponsiveValue /** Sets the shape of the avatar to a square if true. If false, the avatar will be circular. */ square?: boolean - /** URL of the avatar image. */ - src: string + /** URL of the avatar image. When provided, the image takes precedence over the placeholder. */ + src?: string + /** Text to display when no image URL is provided, such as a user's initials. */ + placeholder?: string /** Provide alt text when the Avatar is used without the user's name next to it. */ alt?: string /** Additional class name. */ className?: string -} & React.ComponentPropsWithoutRef<'img'> +} & Omit, 'src'> -const Avatar = React.forwardRef(function Avatar( - {alt = '', size = DEFAULT_AVATAR_SIZE, square = false, className, style, ...rest}, +const Avatar = React.forwardRef(function Avatar( + {alt = '', size = DEFAULT_AVATAR_SIZE, square = false, placeholder, src, className, style, ...rest}, ref, ) { const isResponsive = isResponsiveValue(size) @@ -34,26 +36,42 @@ const Avatar = React.forwardRef(function Avatar( cssSizeVars['--avatarSize-regular'] = `${size}px` } + const sharedProps = { + 'data-component': 'Avatar', + className: clsx(className, classes.Avatar), + ref, + 'data-responsive': isResponsive ? '' : undefined, + 'data-square': square ? '' : undefined, + style: style + ? { + ...cssSizeVars, + ...style, + } + : (cssSizeVars as React.CSSProperties), + ...rest, + } + + if (src) { + return ( + {alt} + ) + } + return ( - {alt} + + {placeholder} + ) }) From bcb4bcab522971375c43b8ecd1fa9e483a683e56 Mon Sep 17 00:00:00 2001 From: Naveed Sajjad <88137903+naveedsajjdawan@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:43:27 +0500 Subject: [PATCH 2/9] style(Avatar): center and scale text placeholders --- packages/react/src/Avatar/Avatar.module.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/react/src/Avatar/Avatar.module.css b/packages/react/src/Avatar/Avatar.module.css index fc67531b9a7..6641d530df4 100644 --- a/packages/react/src/Avatar/Avatar.module.css +++ b/packages/react/src/Avatar/Avatar.module.css @@ -10,6 +10,17 @@ /* stylelint-disable-next-line primer/box-shadow */ box-shadow: 0 0 0 1px var(--avatar-borderColor); + &:where([data-placeholder]) { + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--bgColor-muted); + color: var(--fgColor-muted); + font-size: calc(var(--avatarSize-regular) * 0.4); + font-weight: var(--base-text-weight-semibold); + white-space: nowrap; + } + &:where([data-square]) { /* stylelint-disable-next-line primer/borders */ border-radius: clamp(4px, calc(var(--avatarSize-regular) - 24px), var(--borderRadius-medium)); From 6659029907389dbb1b172a1c421dd0d2d286fc21 Mon Sep 17 00:00:00 2001 From: Naveed Sajjad <88137903+naveedsajjdawan@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:43:34 +0500 Subject: [PATCH 3/9] test(Avatar): cover text placeholder rendering --- packages/react/src/Avatar/Avatar.test.tsx | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/react/src/Avatar/Avatar.test.tsx b/packages/react/src/Avatar/Avatar.test.tsx index ba816856822..314934a2190 100644 --- a/packages/react/src/Avatar/Avatar.test.tsx +++ b/packages/react/src/Avatar/Avatar.test.tsx @@ -36,6 +36,31 @@ describe('Avatar', () => { expect(avatar).toHaveAttribute('src', 'primer.png') }) + it('renders a text placeholder when src is omitted', () => { + render() + const avatar = screen.getByTestId('avatar') + expect(avatar).toHaveTextContent('NS') + expect(avatar).toHaveAttribute('role', 'img') + expect(avatar).toHaveAttribute('aria-label', 'NS') + expect(avatar).toHaveAttribute('data-placeholder') + expect(avatar).toHaveAttribute('data-component', 'Avatar') + expect(avatar).not.toHaveAttribute('src') + }) + + it('prefers the image when both src and placeholder are provided', () => { + render() + const avatar = screen.getByTestId('avatar') + expect(avatar.tagName).toBe('IMG') + expect(avatar).toHaveAttribute('src', 'primer.png') + expect(avatar).toHaveAttribute('alt', 'Naveed') + expect(avatar).not.toHaveAttribute('data-placeholder') + }) + + it('uses alt text as the accessible name for a placeholder when provided', () => { + render() + expect(screen.getByTestId('avatar')).toHaveAttribute('aria-label', 'Naveed Sajjad') + }) + it('should support the `style` prop without overriding internal styles', () => { render( Date: Wed, 21 Oct 2026 13:43:41 +0500 Subject: [PATCH 4/9] fix(Avatar): preserve branch-specific ref types --- packages/react/src/Avatar/Avatar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react/src/Avatar/Avatar.tsx b/packages/react/src/Avatar/Avatar.tsx index a3324234d10..d98dfb1bca3 100644 --- a/packages/react/src/Avatar/Avatar.tsx +++ b/packages/react/src/Avatar/Avatar.tsx @@ -39,7 +39,6 @@ const Avatar = React.forwardRef(function Avatar( const sharedProps = { 'data-component': 'Avatar', className: clsx(className, classes.Avatar), - ref, 'data-responsive': isResponsive ? '' : undefined, 'data-square': square ? '' : undefined, style: style @@ -55,6 +54,7 @@ const Avatar = React.forwardRef(function Avatar( return ( } alt={alt} width={isResponsive ? undefined : size} height={isResponsive ? undefined : size} @@ -66,6 +66,7 @@ const Avatar = React.forwardRef(function Avatar( return ( } role="img" aria-label={alt || placeholder || undefined} data-placeholder="" From b3f34ef5710c64be3376df538f637e9cb7c680e7 Mon Sep 17 00:00:00 2001 From: Naveed Sajjad <88137903+naveedsajjdawan@users.noreply.github.com> Date: Mon, 2 Nov 2026 14:34:05 +0500 Subject: [PATCH 5/9] chore: add Avatar placeholder changeset --- .changeset/small-avatars-smile.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/small-avatars-smile.md diff --git a/.changeset/small-avatars-smile.md b/.changeset/small-avatars-smile.md new file mode 100644 index 00000000000..24e0458bdec --- /dev/null +++ b/.changeset/small-avatars-smile.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Add text placeholder support to Avatar when no image source is provided. From 9bfb213f67cfc5affd1d7eff0f323cbd736227e5 Mon Sep 17 00:00:00 2001 From: Naveed Sajjad <88137903+naveedsajjdawan@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:26:26 +0500 Subject: [PATCH 6/9] fix(Avatar): use Primer typography token for placeholder --- packages/react/src/Avatar/Avatar.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/Avatar/Avatar.module.css b/packages/react/src/Avatar/Avatar.module.css index 6641d530df4..2f157b70a5f 100644 --- a/packages/react/src/Avatar/Avatar.module.css +++ b/packages/react/src/Avatar/Avatar.module.css @@ -16,7 +16,7 @@ justify-content: center; background: var(--bgColor-muted); color: var(--fgColor-muted); - font-size: calc(var(--avatarSize-regular) * 0.4); + font-size: var(--text-body-size-small); font-weight: var(--base-text-weight-semibold); white-space: nowrap; } From 9390525965357deab1a14f0670f3453ec3208eff Mon Sep 17 00:00:00 2001 From: Naveed Sajjad <88137903+naveedsajjdawan@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:26:36 +0500 Subject: [PATCH 7/9] docs(Avatar): document text placeholder prop --- packages/react/src/Avatar/Avatar.docs.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/react/src/Avatar/Avatar.docs.json b/packages/react/src/Avatar/Avatar.docs.json index bfe70bf9206..8de0b951ae1 100644 --- a/packages/react/src/Avatar/Avatar.docs.json +++ b/packages/react/src/Avatar/Avatar.docs.json @@ -15,6 +15,9 @@ }, { "id": "components-avatar-features--size-responsive" + }, + { + "id": "components-avatar-features--text-placeholder" } ], "importPath": "@primer/react", @@ -41,8 +44,14 @@ "name": "src", "type": "string", "required": false, - "description": "URL of the avatar image.", + "description": "URL of the avatar image. When provided, the image takes precedence over the placeholder.", "defaultValue": "" + }, + { + "name": "placeholder", + "type": "string", + "required": false, + "description": "Text to display when no image URL is provided, such as a user's initials." } ], "subcomponents": [] From cda86b45417a9368ff47a8e497a9ee7481ad94fe Mon Sep 17 00:00:00 2001 From: Naveed Sajjad <88137903+naveedsajjdawan@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:26:56 +0500 Subject: [PATCH 8/9] test(Avatar): add text placeholder visual regression story --- packages/react/src/Avatar/Avatar.features.stories.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/react/src/Avatar/Avatar.features.stories.tsx b/packages/react/src/Avatar/Avatar.features.stories.tsx index 6627d744d7b..fafbf2ba653 100644 --- a/packages/react/src/Avatar/Avatar.features.stories.tsx +++ b/packages/react/src/Avatar/Avatar.features.stories.tsx @@ -79,3 +79,12 @@ export const SizeResponsive = () => ( /> ) + +export const TextPlaceholder = () => ( +
+ + + + +
+) From 171a718ead3cc4dcc940151c8f44848856a249c0 Mon Sep 17 00:00:00 2001 From: Naveed Sajjad <88137903+naveedsajjdawan@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:33:27 +0500 Subject: [PATCH 9/9] test(Avatar): include text placeholder in VRT --- e2e/components/Avatar.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/e2e/components/Avatar.test.ts b/e2e/components/Avatar.test.ts index 00d706cc0e1..b398b91601b 100644 --- a/e2e/components/Avatar.test.ts +++ b/e2e/components/Avatar.test.ts @@ -19,6 +19,10 @@ const stories = [ title: 'Square', id: 'components-avatar-features--square', }, + { + title: 'Text Placeholder', + id: 'components-avatar-features--text-placeholder', + }, ] as const test.describe('Avatar', () => {