diff --git a/web/src/lib/components/HorizontalScrollRow.svelte b/web/src/lib/components/HorizontalScrollRow.svelte
new file mode 100644
index 00000000..fe953700
--- /dev/null
+++ b/web/src/lib/components/HorizontalScrollRow.svelte
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+ {#each items as it, i (i)}
+ {@render item?.(it, i)}
+ {/each}
+
+
+
+
+
+
diff --git a/web/src/lib/components/HorizontalScrollRow.test.ts b/web/src/lib/components/HorizontalScrollRow.test.ts
new file mode 100644
index 00000000..55ee2e9d
--- /dev/null
+++ b/web/src/lib/components/HorizontalScrollRow.test.ts
@@ -0,0 +1,32 @@
+import { afterEach, describe, expect, test, vi } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/svelte';
+import HorizontalScrollRow from './HorizontalScrollRow.svelte';
+
+afterEach(() => vi.clearAllMocks());
+
+describe('HorizontalScrollRow', () => {
+ test('renders left and right scroll buttons', () => {
+ render(HorizontalScrollRow, { props: { items: ['a', 'b', 'c'] } });
+ expect(screen.getByRole('button', { name: /scroll left/i })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /scroll right/i })).toBeInTheDocument();
+ });
+
+ test('left button starts disabled (scrollLeft is 0)', () => {
+ render(HorizontalScrollRow, { props: { items: ['a'] } });
+ expect(screen.getByRole('button', { name: /scroll left/i })).toBeDisabled();
+ });
+
+ test('clicking right button calls scrollBy on the container', async () => {
+ const { container } = render(HorizontalScrollRow, { props: { items: ['a', 'b'] } });
+ const scroller = container.querySelector('[data-testid="scroll-container"]') as HTMLElement;
+ const spy = vi.fn();
+ scroller.scrollBy = spy as unknown as HTMLElement['scrollBy'];
+ Object.defineProperty(scroller, 'clientWidth', { value: 800, configurable: true });
+ Object.defineProperty(scroller, 'scrollWidth', { value: 2000, configurable: true });
+ Object.defineProperty(scroller, 'scrollLeft', { value: 0, configurable: true, writable: true });
+
+ await fireEvent.click(screen.getByRole('button', { name: /scroll right/i }));
+ expect(spy).toHaveBeenCalledWith({ left: expect.any(Number), behavior: 'smooth' });
+ expect(spy.mock.calls[0][0].left).toBeGreaterThan(0);
+ });
+});