diff --git a/web/src/lib/components/LibrarySkeleton.svelte b/web/src/lib/components/LibrarySkeleton.svelte
new file mode 100644
index 00000000..deef89f9
--- /dev/null
+++ b/web/src/lib/components/LibrarySkeleton.svelte
@@ -0,0 +1,48 @@
+
+
+{#if variant === 'list'}
+
+ {#each Array.from({ length: count }) as _, i (i)}
+
+
+
+
+
+ {/each}
+
+{:else if variant === 'grid'}
+
+ {#each Array.from({ length: count }) as _, i (i)}
+
+ {/each}
+
+{:else}
+
+
+
+ {#each Array.from({ length: 10 }) as _, i (i)}
+
+ {/each}
+
+
+{/if}
diff --git a/web/src/lib/components/LibrarySkeleton.test.ts b/web/src/lib/components/LibrarySkeleton.test.ts
new file mode 100644
index 00000000..78784f39
--- /dev/null
+++ b/web/src/lib/components/LibrarySkeleton.test.ts
@@ -0,0 +1,23 @@
+import { describe, expect, test } from 'vitest';
+import { render, screen } from '@testing-library/svelte';
+import LibrarySkeleton from './LibrarySkeleton.svelte';
+
+describe('LibrarySkeleton', () => {
+ test('variant="list" renders count placeholder rows', () => {
+ render(LibrarySkeleton, { props: { variant: 'list', count: 5 } });
+ const container = screen.getByTestId('skeleton-list');
+ expect(container.querySelectorAll('[data-skeleton-row]').length).toBe(5);
+ });
+
+ test('variant="grid" renders count placeholder cards', () => {
+ render(LibrarySkeleton, { props: { variant: 'grid', count: 6 } });
+ const container = screen.getByTestId('skeleton-grid');
+ expect(container.querySelectorAll('[data-skeleton-card]').length).toBe(6);
+ });
+
+ test('variant="album" renders a hero placeholder with cover block + bars', () => {
+ render(LibrarySkeleton, { props: { variant: 'album' } });
+ expect(screen.getByTestId('skeleton-album')).toBeInTheDocument();
+ expect(screen.getByTestId('skeleton-album-cover')).toBeInTheDocument();
+ });
+});