diff --git a/web/src/lib/components/ArtistRow.svelte b/web/src/lib/components/ArtistRow.svelte
new file mode 100644
index 00000000..92aee7ad
--- /dev/null
+++ b/web/src/lib/components/ArtistRow.svelte
@@ -0,0 +1,23 @@
+
+
+
+
+ {initial}
+
+ {artist.name}
+ {countLabel}
+ ›
+
diff --git a/web/src/lib/components/ArtistRow.test.ts b/web/src/lib/components/ArtistRow.test.ts
new file mode 100644
index 00000000..5dec7fe4
--- /dev/null
+++ b/web/src/lib/components/ArtistRow.test.ts
@@ -0,0 +1,29 @@
+import { describe, expect, test } from 'vitest';
+import { render, screen } from '@testing-library/svelte';
+import ArtistRow from './ArtistRow.svelte';
+import type { ArtistRef } from '$lib/api/types';
+
+const artist: ArtistRef = { id: 'abc', name: 'alice', album_count: 12 };
+
+describe('ArtistRow', () => {
+ test('renders initial, name, album count inside a link to /artists/:id', () => {
+ render(ArtistRow, { props: { artist } });
+
+ const link = screen.getByRole('link');
+ expect(link).toHaveAttribute('href', '/artists/abc');
+ expect(link).toHaveTextContent('alice');
+ expect(link).toHaveTextContent('12 albums');
+ expect(link).toHaveTextContent('A'); // initial letter
+ });
+
+ test('singular "1 album" when album_count is 1', () => {
+ render(ArtistRow, { props: { artist: { ...artist, album_count: 1 } } });
+ expect(screen.getByRole('link')).toHaveTextContent('1 album');
+ });
+
+ test('empty name still renders a safe initial', () => {
+ render(ArtistRow, { props: { artist: { ...artist, name: '' } } });
+ // No crash; initial container present but empty or a placeholder char.
+ expect(screen.getByRole('link')).toBeInTheDocument();
+ });
+});