diff --git a/frontend/src/lib/shared/primitives/Avatar.svelte b/frontend/src/lib/shared/primitives/Avatar.svelte
new file mode 100644
index 00000000..a7f25870
--- /dev/null
+++ b/frontend/src/lib/shared/primitives/Avatar.svelte
@@ -0,0 +1,94 @@
+
+
+{#if overflow !== undefined}
+
+
+ +{overflow}
+
+{:else if placeholder}
+
+
+{:else if decorative}
+
+
+ {avatar.initials}
+
+{:else}
+
+
+ {avatar.initials}
+
+{/if}
diff --git a/frontend/src/lib/shared/primitives/Avatar.svelte.spec.ts b/frontend/src/lib/shared/primitives/Avatar.svelte.spec.ts
new file mode 100644
index 00000000..243db6bc
--- /dev/null
+++ b/frontend/src/lib/shared/primitives/Avatar.svelte.spec.ts
@@ -0,0 +1,105 @@
+import { describe, it, expect, afterEach } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import { page } from 'vitest/browser';
+
+import Avatar from './Avatar.svelte';
+
+afterEach(() => cleanup());
+
+function getAvatar(): HTMLElement | null {
+ return document.querySelector('[data-testid="avatar"]');
+}
+
+describe('Avatar', () => {
+ it('renders initials from name', async () => {
+ render(Avatar, { name: 'Anna Raddatz' });
+ await expect.element(page.getByText('AR')).toBeInTheDocument();
+ });
+
+ it('applies size-26 class for size=26', async () => {
+ render(Avatar, { name: 'Anna Raddatz', size: 26 });
+ const el = getAvatar();
+ expect(el).not.toBeNull();
+ expect(el!.className).toMatch(/h-\[26px\]/);
+ expect(el!.className).toMatch(/w-\[26px\]/);
+ });
+
+ it('applies size-28 class for size=28', async () => {
+ render(Avatar, { name: 'Karl Müller', size: 28 });
+ const el = getAvatar();
+ expect(el).not.toBeNull();
+ expect(el!.className).toMatch(/h-\[28px\]/);
+ expect(el!.className).toMatch(/w-\[28px\]/);
+ });
+
+ it('applies size-40 class for size=40', async () => {
+ render(Avatar, { name: 'Karl Müller', size: 40 });
+ const el = getAvatar();
+ expect(el).not.toBeNull();
+ expect(el!.className).toMatch(/h-\[40px\]/);
+ expect(el!.className).toMatch(/w-\[40px\]/);
+ });
+
+ it('applies size-48 class for size=48', async () => {
+ render(Avatar, { name: 'Herbert Cram', size: 48 });
+ const el = getAvatar();
+ expect(el).not.toBeNull();
+ expect(el!.className).toMatch(/h-\[48px\]/);
+ expect(el!.className).toMatch(/w-\[48px\]/);
+ });
+
+ it('stacked variant adds -ml-1.5 and ring-2 ring-surface', async () => {
+ render(Avatar, { name: 'Anna Raddatz', size: 26, stacked: true });
+ const el = getAvatar();
+ expect(el).not.toBeNull();
+ expect(el!.className).toMatch(/-ml-1\.5/);
+ expect(el!.className).toMatch(/ring-2/);
+ expect(el!.className).toMatch(/ring-surface/);
+ });
+
+ it('non-stacked variant does not have -ml-1.5', async () => {
+ render(Avatar, { name: 'Anna Raddatz', size: 26, stacked: false });
+ const el = getAvatar();
+ expect(el).not.toBeNull();
+ expect(el!.className).not.toMatch(/-ml-1\.5/);
+ });
+
+ it('decorative mode: aria-hidden=true when decorative prop is true', async () => {
+ render(Avatar, { name: 'Anna Raddatz', decorative: true });
+ const el = getAvatar();
+ expect(el).not.toBeNull();
+ expect(el!.getAttribute('aria-hidden')).toBe('true');
+ });
+
+ it('standalone mode: role=img and aria-label=name when decorative is false', async () => {
+ render(Avatar, { name: 'Herbert Cram', decorative: false });
+ await expect.element(page.getByRole('img', { name: 'Herbert Cram' })).toBeInTheDocument();
+ });
+
+ it('renders overflow +N chip when overflow prop is set', async () => {
+ render(Avatar, { overflow: 3 });
+ await expect.element(page.getByText('+3')).toBeInTheDocument();
+ });
+
+ it('renders placeholder (dashed border) when placeholder prop is true', async () => {
+ render(Avatar, { placeholder: true, size: 26 });
+ const el = getAvatar();
+ expect(el).not.toBeNull();
+ expect(el!.className).toMatch(/border-dashed/);
+ });
+
+ it('has rounded-full class', async () => {
+ render(Avatar, { name: 'Karl Müller', size: 40 });
+ const el = getAvatar();
+ expect(el).not.toBeNull();
+ expect(el!.className).toMatch(/rounded-full/);
+ });
+
+ it('stacked variant carries ring classes (dark mode edge on dark surface)', async () => {
+ render(Avatar, { name: 'Karl Müller', size: 26, stacked: true });
+ const el = getAvatar();
+ expect(el).not.toBeNull();
+ // ring-2 ring-surface keeps avatar edges visible on dark surface (#011526)
+ expect(el!.className).toMatch(/ring/);
+ });
+});