diff --git a/.changeset/moody-socks-mate.md b/.changeset/moody-socks-mate.md
new file mode 100644
index 0000000000..9bee9120ad
--- /dev/null
+++ b/.changeset/moody-socks-mate.md
@@ -0,0 +1,5 @@
+---
+'@backstage/canon': patch
+---
+
+Add new Avatar component to Canon.
diff --git a/canon-docs/src/app/(docs)/components/avatar/page.mdx b/canon-docs/src/app/(docs)/components/avatar/page.mdx
new file mode 100644
index 0000000000..b46caf8d62
--- /dev/null
+++ b/canon-docs/src/app/(docs)/components/avatar/page.mdx
@@ -0,0 +1,75 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { Tabs } from '@/components/Tabs';
+import { CodeBlock } from '@/components/CodeBlock';
+import { AvatarSnippet } from '@/snippets/stories-snippets';
+import { avatarPropDefs } from './props';
+
+# Avatar
+
+An avatar component with a fallback for initials.
+
+}
+ code={``}
+/>
+
+
+
+ Usage
+ Theming
+
+
+ `}
+ />
+
+
+ We recommend starting with our [global tokens](/theme/theming) to customize the library and align it with
+ your brand. For additional flexibility, you can use the provided class names for each element listed below.
+ - `canon-AvatarRoot`
+ - `canon-AvatarRoot[data-size='small']`
+ - `canon-AvatarRoot[data-size='medium']`
+ - `canon-AvatarRoot[data-size='large']`
+ - `canon-AvatarImage`
+ - `canon-AvatarFallback`
+
+
+
+## API reference
+
+
+
+## Examples
+
+### Sizes
+
+Avatar sizes can be set using the `size` prop.
+
+}
+ code={`
+
+
+
+ `}
+/>
+
+### Fallback
+
+If the image is not available, the avatar will show the initials of the name.
+
+}
+ code={``}
+/>
diff --git a/canon-docs/src/app/(docs)/components/avatar/props.ts b/canon-docs/src/app/(docs)/components/avatar/props.ts
new file mode 100644
index 0000000000..e86c67a41e
--- /dev/null
+++ b/canon-docs/src/app/(docs)/components/avatar/props.ts
@@ -0,0 +1,19 @@
+import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
+import type { PropDef } from '@/utils/propDefs';
+
+export const avatarPropDefs: Record = {
+ src: {
+ type: 'string',
+ },
+ name: {
+ type: 'string',
+ },
+ size: {
+ type: 'enum',
+ values: ['small', 'medium', 'large'],
+ default: 'medium',
+ responsive: true,
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/canon-docs/src/snippets/stories-snippets.tsx b/canon-docs/src/snippets/stories-snippets.tsx
index 6b54f5dc4d..83cc866050 100644
--- a/canon-docs/src/snippets/stories-snippets.tsx
+++ b/canon-docs/src/snippets/stories-snippets.tsx
@@ -15,6 +15,7 @@ import * as FlexStories from '../../../packages/canon/src/components/Flex/Flex.s
import * as SelectStories from '../../../packages/canon/src/components/Select/Select.stories';
import * as MenuStories from '../../../packages/canon/src/components/Menu/Menu.stories';
import * as LinkStories from '../../../packages/canon/src/components/Link/Link.stories';
+import * as AvatarStories from '../../../packages/canon/src/components/Avatar/Avatar.stories';
export const BoxSnippet = ({ story }: { story: keyof typeof BoxStories }) => {
const stories = composeStories(BoxStories);
@@ -141,3 +142,14 @@ export const LinkSnippet = ({ story }: { story: keyof typeof LinkStories }) => {
return StoryComponent ? : null;
};
+
+export const AvatarSnippet = ({
+ story,
+}: {
+ story: keyof typeof AvatarStories;
+}) => {
+ const stories = composeStories(AvatarStories);
+ const StoryComponent = stories[story as keyof typeof stories];
+
+ return StoryComponent ? : null;
+};
diff --git a/canon-docs/src/utils/data.ts b/canon-docs/src/utils/data.ts
index 48274ea1a2..d1e9277e57 100644
--- a/canon-docs/src/utils/data.ts
+++ b/canon-docs/src/utils/data.ts
@@ -66,6 +66,11 @@ export const layoutComponents: Page[] = [
];
export const components: Page[] = [
+ {
+ title: 'Avatar',
+ slug: 'avatar',
+ status: 'alpha',
+ },
{
title: 'Button',
slug: 'button',
diff --git a/packages/canon/css/avatar.css b/packages/canon/css/avatar.css
new file mode 100644
index 0000000000..c1f4cca59e
--- /dev/null
+++ b/packages/canon/css/avatar.css
@@ -0,0 +1,49 @@
+.canon-AvatarRoot {
+ vertical-align: middle;
+ user-select: none;
+ color: var(--canon-fg-primary);
+ background-color: var(--canon-bg-surface-2);
+ border-radius: 100%;
+ justify-content: center;
+ align-items: center;
+ width: 2rem;
+ height: 2rem;
+ font-size: 1rem;
+ font-weight: 500;
+ line-height: 1;
+ display: inline-flex;
+ overflow: hidden;
+}
+
+.canon-AvatarRoot[data-size="small"] {
+ width: 1.5rem;
+ height: 1.5rem;
+}
+
+.canon-AvatarRoot[data-size="medium"] {
+ width: 2rem;
+ height: 2rem;
+}
+
+.canon-AvatarRoot[data-size="large"] {
+ width: 3rem;
+ height: 3rem;
+}
+
+.canon-AvatarImage {
+ object-fit: cover;
+ width: 100%;
+ height: 100%;
+}
+
+.canon-AvatarFallback {
+ width: 100%;
+ height: 100%;
+ font-size: var(--canon-font-size-3);
+ font-weight: var(--canon-font-weight-regular);
+ box-shadow: inset 0 0 0 1px var(--canon-border);
+ border-radius: var(--canon-radius-full);
+ justify-content: center;
+ align-items: center;
+ display: flex;
+}
diff --git a/packages/canon/css/components.css b/packages/canon/css/components.css
index bb1ba10018..ed294b98f4 100644
--- a/packages/canon/css/components.css
+++ b/packages/canon/css/components.css
@@ -1,3 +1,53 @@
+.canon-AvatarRoot {
+ vertical-align: middle;
+ user-select: none;
+ color: var(--canon-fg-primary);
+ background-color: var(--canon-bg-surface-2);
+ border-radius: 100%;
+ justify-content: center;
+ align-items: center;
+ width: 2rem;
+ height: 2rem;
+ font-size: 1rem;
+ font-weight: 500;
+ line-height: 1;
+ display: inline-flex;
+ overflow: hidden;
+}
+
+.canon-AvatarRoot[data-size="small"] {
+ width: 1.5rem;
+ height: 1.5rem;
+}
+
+.canon-AvatarRoot[data-size="medium"] {
+ width: 2rem;
+ height: 2rem;
+}
+
+.canon-AvatarRoot[data-size="large"] {
+ width: 3rem;
+ height: 3rem;
+}
+
+.canon-AvatarImage {
+ object-fit: cover;
+ width: 100%;
+ height: 100%;
+}
+
+.canon-AvatarFallback {
+ width: 100%;
+ height: 100%;
+ font-size: var(--canon-font-size-3);
+ font-weight: var(--canon-font-weight-regular);
+ box-shadow: inset 0 0 0 1px var(--canon-border);
+ border-radius: var(--canon-radius-full);
+ justify-content: center;
+ align-items: center;
+ display: flex;
+}
+
.canon-Box {
font-family: var(--canon-font-regular);
font-weight: var(--canon-font-weight-regular);
diff --git a/packages/canon/css/styles.css b/packages/canon/css/styles.css
index 473b8f5d45..942c33f138 100644
--- a/packages/canon/css/styles.css
+++ b/packages/canon/css/styles.css
@@ -9222,6 +9222,56 @@
--canon-scrollbar-thumb: #575757;
}
+.canon-AvatarRoot {
+ vertical-align: middle;
+ user-select: none;
+ color: var(--canon-fg-primary);
+ background-color: var(--canon-bg-surface-2);
+ border-radius: 100%;
+ justify-content: center;
+ align-items: center;
+ width: 2rem;
+ height: 2rem;
+ font-size: 1rem;
+ font-weight: 500;
+ line-height: 1;
+ display: inline-flex;
+ overflow: hidden;
+}
+
+.canon-AvatarRoot[data-size="small"] {
+ width: 1.5rem;
+ height: 1.5rem;
+}
+
+.canon-AvatarRoot[data-size="medium"] {
+ width: 2rem;
+ height: 2rem;
+}
+
+.canon-AvatarRoot[data-size="large"] {
+ width: 3rem;
+ height: 3rem;
+}
+
+.canon-AvatarImage {
+ object-fit: cover;
+ width: 100%;
+ height: 100%;
+}
+
+.canon-AvatarFallback {
+ width: 100%;
+ height: 100%;
+ font-size: var(--canon-font-size-3);
+ font-weight: var(--canon-font-weight-regular);
+ box-shadow: inset 0 0 0 1px var(--canon-border);
+ border-radius: var(--canon-radius-full);
+ justify-content: center;
+ align-items: center;
+ display: flex;
+}
+
.canon-Box {
font-family: var(--canon-font-regular);
font-weight: var(--canon-font-weight-regular);
diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md
index 1e65a51dca..fbc1a68542 100644
--- a/packages/canon/report.api.md
+++ b/packages/canon/report.api.md
@@ -3,6 +3,7 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
+import { Avatar as Avatar_2 } from '@base-ui-components/react/avatar';
import { Breakpoint as Breakpoint_2 } from '@backstage/canon';
import { ChangeEvent } from 'react';
import { Context } from 'react';
@@ -52,6 +53,22 @@ export type AsProps =
| 'dl'
| 'dt';
+// @public (undocumented)
+export const Avatar: ForwardRefExoticComponent<
+ AvatarProps & RefAttributes
+>;
+
+// @public (undocumented)
+export interface AvatarProps
+ extends React.ComponentPropsWithoutRef {
+ // (undocumented)
+ name: string;
+ // (undocumented)
+ size?: 'small' | 'medium' | 'large';
+ // (undocumented)
+ src: string;
+}
+
// @public (undocumented)
export type BooleanPropDef = {
type: 'boolean';
diff --git a/packages/canon/src/components/Avatar/Avatar.stories.tsx b/packages/canon/src/components/Avatar/Avatar.stories.tsx
new file mode 100644
index 0000000000..ce9e38a5b0
--- /dev/null
+++ b/packages/canon/src/components/Avatar/Avatar.stories.tsx
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { Meta, StoryObj } from '@storybook/react';
+import { Avatar } from './index';
+import { Flex } from '@backstage/canon';
+
+const meta = {
+ title: 'Components/Avatar',
+ component: Avatar,
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ src: 'https://avatars.githubusercontent.com/u/1540635?v=4',
+ name: 'Charles de Dreuille',
+ },
+};
+
+export const Fallback: Story = {
+ args: {
+ ...Default.args,
+ src: 'https://avatars.githubusercontent.com/u/15406AAAAAAAAA',
+ },
+};
+
+export const Sizes: Story = {
+ args: {
+ ...Default.args,
+ },
+ render: args => (
+
+
+
+
+
+ ),
+};
diff --git a/packages/canon/src/components/Avatar/Avatar.styles.css b/packages/canon/src/components/Avatar/Avatar.styles.css
new file mode 100644
index 0000000000..b2cbd140e2
--- /dev/null
+++ b/packages/canon/src/components/Avatar/Avatar.styles.css
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.canon-AvatarRoot {
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ vertical-align: middle;
+ border-radius: 100%;
+ user-select: none;
+ font-weight: 500;
+ color: var(--canon-fg-primary);
+ background-color: var(--canon-bg-surface-2);
+ font-size: 1rem;
+ line-height: 1;
+ overflow: hidden;
+ height: 2rem;
+ width: 2rem;
+}
+
+.canon-AvatarRoot[data-size='small'] {
+ height: 1.5rem;
+ width: 1.5rem;
+}
+
+.canon-AvatarRoot[data-size='medium'] {
+ height: 2rem;
+ width: 2rem;
+}
+
+.canon-AvatarRoot[data-size='large'] {
+ height: 3rem;
+ width: 3rem;
+}
+
+.canon-AvatarImage {
+ object-fit: cover;
+ height: 100%;
+ width: 100%;
+}
+
+.canon-AvatarFallback {
+ align-items: center;
+ display: flex;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
+ font-size: var(--canon-font-size-3);
+ font-weight: var(--canon-font-weight-regular);
+ box-shadow: inset 0 0 0 1px var(--canon-border);
+ border-radius: var(--canon-radius-full);
+}
diff --git a/packages/canon/src/components/Avatar/Avatar.tsx b/packages/canon/src/components/Avatar/Avatar.tsx
new file mode 100644
index 0000000000..472bec39fb
--- /dev/null
+++ b/packages/canon/src/components/Avatar/Avatar.tsx
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { forwardRef, ElementRef } from 'react';
+import { Avatar as AvatarPrimitive } from '@base-ui-components/react/avatar';
+import clsx from 'clsx';
+import { AvatarProps } from './types';
+
+/** @public */
+export const Avatar = forwardRef<
+ ElementRef,
+ AvatarProps
+>(({ className, src, name, size = 'medium', ...props }, ref) => (
+
+
+
+ {(name || '')
+ .split(' ')
+ .map(word => word[0])
+ .join('')
+ .toLocaleUpperCase('en-US')
+ .slice(0, 2)}
+
+
+));
+
+Avatar.displayName = AvatarPrimitive.Root.displayName;
diff --git a/packages/canon/src/components/Avatar/index.ts b/packages/canon/src/components/Avatar/index.ts
new file mode 100644
index 0000000000..d5ab42beb2
--- /dev/null
+++ b/packages/canon/src/components/Avatar/index.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { Avatar } from './Avatar';
+export type { AvatarProps } from './types';
diff --git a/packages/canon/src/components/Avatar/types.ts b/packages/canon/src/components/Avatar/types.ts
new file mode 100644
index 0000000000..b2e6cf39cf
--- /dev/null
+++ b/packages/canon/src/components/Avatar/types.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Avatar } from '@base-ui-components/react/avatar';
+
+/** @public */
+export interface AvatarProps
+ extends React.ComponentPropsWithoutRef {
+ src: string;
+ name: string;
+ size?: 'small' | 'medium' | 'large';
+}
diff --git a/packages/canon/src/css/components.css b/packages/canon/src/css/components.css
index 7dee4afb97..8abef12afe 100644
--- a/packages/canon/src/css/components.css
+++ b/packages/canon/src/css/components.css
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+@import '../components/Avatar/Avatar.styles.css';
@import '../components/Box/styles.css';
@import '../components/Button/styles.css';
@import '../components/DataTable/Root/DataTableRoot.styles.css';
diff --git a/packages/canon/src/index.ts b/packages/canon/src/index.ts
index 9e9e401655..e5e38e696c 100644
--- a/packages/canon/src/index.ts
+++ b/packages/canon/src/index.ts
@@ -32,6 +32,7 @@ export * from './components/Text';
export * from './components/Heading';
// UI components
+export * from './components/Avatar';
export * from './components/Button';
export * from './components/DataTable';
export * from './components/Icon';