diff --git a/web/src/routes/artists/[id]/+page.svelte b/web/src/routes/artists/[id]/+page.svelte
new file mode 100644
index 00000000..292c2286
--- /dev/null
+++ b/web/src/routes/artists/[id]/+page.svelte
@@ -0,0 +1,51 @@
+
+
+
+
← Library
+
+ {#if notFound}
+
+ {:else if query.isError}
+
+ {:else if showSkeleton.value && !query.data}
+
+ {:else if query.data}
+ {@const detail = query.data}
+
+
+ {#if detail.albums.length === 0}
+
This artist has no albums in the library.
+ {:else}
+
+ {#each detail.albums as album (album.id)}
+
+ {/each}
+
+ {/if}
+ {/if}
+
diff --git a/web/src/routes/artists/[id]/artist.test.ts b/web/src/routes/artists/[id]/artist.test.ts
new file mode 100644
index 00000000..3d3381b3
--- /dev/null
+++ b/web/src/routes/artists/[id]/artist.test.ts
@@ -0,0 +1,93 @@
+import { afterEach, describe, expect, test, vi } from 'vitest';
+import { render, screen } from '@testing-library/svelte';
+import { flushSync } from 'svelte';
+import { mockQuery } from '../../../test-utils/query';
+import type { ArtistDetail, AlbumRef } from '$lib/api/types';
+
+const state = vi.hoisted(() => ({ pageParams: { id: 'abc' } as Record