diff --git a/frontend/src/lib/shared/primitives/Sparkline.svelte b/frontend/src/lib/shared/primitives/Sparkline.svelte
new file mode 100644
index 00000000..2876d3e1
--- /dev/null
+++ b/frontend/src/lib/shared/primitives/Sparkline.svelte
@@ -0,0 +1,38 @@
+
+
+
+ {#each values as value, i (i)}
+
+ {/each}
+
diff --git a/frontend/src/lib/shared/primitives/Sparkline.svelte.spec.ts b/frontend/src/lib/shared/primitives/Sparkline.svelte.spec.ts
new file mode 100644
index 00000000..228302b0
--- /dev/null
+++ b/frontend/src/lib/shared/primitives/Sparkline.svelte.spec.ts
@@ -0,0 +1,28 @@
+import { describe, it, expect, afterEach } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import Sparkline from './Sparkline.svelte';
+
+afterEach(() => cleanup());
+
+describe('Sparkline', () => {
+ it('renders one bar per value', () => {
+ render(Sparkline, { values: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] });
+ const bars = document.querySelectorAll('[data-testid="sparkline-bar"]');
+ expect(bars).toHaveLength(12);
+ });
+
+ it('scales bar heights relative to the largest value', () => {
+ render(Sparkline, { values: [5, 10, 0] });
+ const bars = document.querySelectorAll('[data-testid="sparkline-bar"]');
+ const h = (i: number) => parseFloat(bars[i].style.height);
+ // 10 is the max → tallest; 5 is half of the max's height; 0 is the shortest.
+ expect(h(1)).toBeGreaterThan(h(0));
+ expect(h(0)).toBeGreaterThan(h(2));
+ });
+
+ it('exposes an accessible label when provided', () => {
+ render(Sparkline, { values: [1, 2, 3], label: 'Monatsdichte' });
+ const img = document.querySelector('[role="img"]');
+ expect(img?.getAttribute('aria-label')).toBe('Monatsdichte');
+ });
+});