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.
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));
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(
/** 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,43 @@ const Avatar = React.forwardRef(function Avatar(
cssSizeVars['--avatarSize-regular'] = `${size}px`
}
+ const sharedProps = {
+ 'data-component': 'Avatar',
+ className: clsx(className, classes.Avatar),
+ 'data-responsive': isResponsive ? '' : undefined,
+ 'data-square': square ? '' : undefined,
+ style: style
+ ? {
+ ...cssSizeVars,
+ ...style,
+ }
+ : (cssSizeVars as React.CSSProperties),
+ ...rest,
+ }
+
+ if (src) {
+ return (
+
}
+ alt={alt}
+ width={isResponsive ? undefined : size}
+ height={isResponsive ? undefined : size}
+ src={src}
+ />
+ )
+ }
+
return (
-
+ }
+ role="img"
+ aria-label={alt || placeholder || undefined}
+ data-placeholder=""
+ >
+ {placeholder}
+
)
})