Skip to content

fix(avatar): Avatar component now always includes alt attribute #5512

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fifty-clocks-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@spectrum-web-components/avatar': minor
---

**Fixed** : Avatar component now always includes alt attribute for improved accessibility even when no label is specified
15 changes: 14 additions & 1 deletion packages/avatar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,17 @@ import { Avatar } from '@spectrum-web-components/avatar';

## Accessibility

The `label` attribute of the `<sp-avatar>` will be passed into the `<img>` element as the `alt` tag for use in defining a textual representation of the image displayed.
The Avatar component is designed to be accessible by default:

- Always includes an `alt` attribute on the image element
- When a `label` is provided, it is used as the `alt` text
- When no `label` is provided, an empty `alt=""` is used to indicate a decorative image
- Supports keyboard navigation when used with `href` or `tabindex`
- Maintains WCAG compliance for non-text content

### Best Practices

- Always provide a meaningful `label` when the avatar represents a user or entity
- Use an empty `label` (or omit it) only when the avatar is purely decorative
- When using `href`, ensure the destination is relevant and accessible
- Consider the context when choosing an appropriate `size`
7 changes: 1 addition & 6 deletions packages/avatar/src/Avatar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
property,
query,
} from '@spectrum-web-components/base/src/decorators.js';
import { ifDefined } from '@spectrum-web-components/base/src/directives.js';
import { LikeAnchor } from '@spectrum-web-components/shared/src/like-anchor.js';
import { Focusable } from '@spectrum-web-components/shared/src/focusable.js';

Expand Down Expand Up @@ -73,11 +72,7 @@ export class Avatar extends LikeAnchor(Focusable) {

protected override render(): TemplateResult {
const avatar = html`
<img
class="image"
alt=${ifDefined(this.label || undefined)}
src=${this.src}
/>
<img class="image" alt=${this.label || ''} src=${this.src} />
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to surface a dev-mode warning that alt text is missing and that the image will be skipped by screen readers if not provided?

Copy link
Contributor

@rubencarvalho rubencarvalho Jun 10, 2025

Choose a reason for hiding this comment

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

Having an empty alt attribute is acceptable for decorative avatars, so I'm afraid flagging it may just add console noise for valid use cases. WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

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

(e.g., the profile picture avatar in Adobe Home is decorative)

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's discuss, because the A11Y team also has concerns: #5513 (comment)

`;
if (this.href) {
return this.renderAnchor({
Expand Down
95 changes: 41 additions & 54 deletions packages/avatar/test/avatar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,53 +16,45 @@ import { testForLitDevWarnings } from '../../../test/testing-helpers';
describe('Avatar', () => {
testForLitDevWarnings(
async () =>
await fixture<Avatar>(
html`
<sp-avatar
label="Shantanu Narayen"
src="https://picsum.photos/500/500"
></sp-avatar>
`
)
);
it('loads accessibly', async () => {
const el = await fixture<Avatar>(
html`
await fixture<Avatar>(html`
<sp-avatar
label="Shantanu Narayen"
src="https://picsum.photos/500/500"
></sp-avatar>
`
);
`)
);
it('loads accessibly', async () => {
const el = await fixture<Avatar>(html`
<sp-avatar
label="Shantanu Narayen"
src="https://picsum.photos/500/500"
></sp-avatar>
`);

await elementUpdated(el);

await expect(el).to.be.accessible();
});
it('loads accessibly with [href]', async () => {
const el = await fixture<Avatar>(
html`
<sp-avatar
label="Shantanu Narayen"
src="https://picsum.photos/500/500"
href="https://adobe.com"
></sp-avatar>
`
);
const el = await fixture<Avatar>(html`
<sp-avatar
label="Shantanu Narayen"
src="https://picsum.photos/500/500"
href="https://adobe.com"
></sp-avatar>
`);

await elementUpdated(el);

await expect(el).to.be.accessible();
});
it('validates `size`', async () => {
const el = await fixture<Avatar>(
html`
<sp-avatar
label="Shantanu Narayen"
src="https://picsum.photos/500/500"
></sp-avatar>
`
);
const el = await fixture<Avatar>(html`
<sp-avatar
label="Shantanu Narayen"
src="https://picsum.photos/500/500"
></sp-avatar>
`);

await elementUpdated(el);

Expand All @@ -81,14 +73,12 @@ describe('Avatar', () => {
expect(el.size).to.equal(600);
});
it('loads with everything set', async () => {
const el = await fixture<Avatar>(
html`
<sp-avatar
label="Shantanu Narayen"
src="https://picsum.photos/500/500"
></sp-avatar>
`
);
const el = await fixture<Avatar>(html`
<sp-avatar
label="Shantanu Narayen"
src="https://picsum.photos/500/500"
></sp-avatar>
`);

await elementUpdated(el);
expect(el).to.not.be.undefined;
Expand All @@ -99,30 +89,27 @@ describe('Avatar', () => {
expect(imageEl.getAttribute('alt')).to.equal('Shantanu Narayen');
});
it('loads with no label', async () => {
const el = await fixture<Avatar>(
html`
<sp-avatar src="https://picsum.photos/500/500"></sp-avatar>
`
);
const el = await fixture<Avatar>(html`
<sp-avatar src="https://picsum.photos/500/500"></sp-avatar>
`);

await elementUpdated(el);
expect(el).to.not.be.undefined;
const imageEl = el.shadowRoot
? (el.shadowRoot.querySelector('img') as HTMLImageElement)
: (el.querySelector('img') as HTMLImageElement);
expect(imageEl.hasAttribute('alt')).to.be.false;
expect(imageEl.hasAttribute('alt')).to.be.true;
expect(imageEl.getAttribute('alt')).to.equal('');
});
it('can receive a `tabindex` without an `href`', async () => {
try {
const el = await fixture<Avatar>(
html`
<sp-avatar
label="Shantanu Narayen"
src="https://picsum.photos/500/500"
tabindex="0"
></sp-avatar>
`
);
const el = await fixture<Avatar>(html`
<sp-avatar
label="Shantanu Narayen"
src="https://picsum.photos/500/500"
tabindex="0"
></sp-avatar>
`);
await elementUpdated(el);
const focusEl = el.focusElement;
expect(focusEl).to.exist;
Expand Down
Loading