diff --git a/frontend/src/lib/timeline/TagChip.svelte b/frontend/src/lib/timeline/TagChip.svelte
new file mode 100644
index 00000000..94546da0
--- /dev/null
+++ b/frontend/src/lib/timeline/TagChip.svelte
@@ -0,0 +1,44 @@
+
+
+
+ {m.timeline_tag_chip_label()}:
+ {#if color}
+
+ {:else}
+
+ {/if}
+ {name}
+
diff --git a/frontend/src/lib/timeline/TagChip.svelte.spec.ts b/frontend/src/lib/timeline/TagChip.svelte.spec.ts
new file mode 100644
index 00000000..4d31199b
--- /dev/null
+++ b/frontend/src/lib/timeline/TagChip.svelte.spec.ts
@@ -0,0 +1,46 @@
+import { describe, it, expect, afterEach } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import * as m from '$lib/paraglide/messages.js';
+import TagChip from './TagChip.svelte';
+
+afterEach(() => cleanup());
+
+describe('TagChip', () => {
+ it('renders the tag name (REQ-008)', () => {
+ render(TagChip, { name: 'Familie', color: 'sage' });
+ expect(document.body.textContent).toContain('Familie');
+ });
+
+ it('prefixes the name with an sr-only theme label and a decorative square (REQ-011)', () => {
+ render(TagChip, { name: 'Krieg', color: 'sienna' });
+ const srOnly = document.querySelector('.sr-only');
+ expect(srOnly?.textContent).toContain(m.timeline_tag_chip_label());
+ const square = document.querySelector('[data-testid="tag-chip-square"]');
+ expect(square?.getAttribute('aria-hidden')).toBe('true');
+ });
+
+ it('applies the color via var(--c-tag-{token}), never raw hex (REQ-009)', () => {
+ render(TagChip, { name: 'Krieg', color: 'sienna' });
+ const square = document.querySelector('[data-testid="tag-chip-square"]') as HTMLElement;
+ expect(square.getAttribute('style')).toContain('var(--c-tag-sienna)');
+ });
+
+ it('renders a neutral chip with no --c-tag- binding when color is null (REQ-007)', () => {
+ render(TagChip, { name: 'Allgemein', color: null });
+ expect(document.body.textContent).toContain('Allgemein');
+ expect(document.body.innerHTML).not.toContain('var(--c-tag-');
+ });
+
+ it('exposes the full name as the chip title so a truncated name stays reachable (REQ-008a)', () => {
+ render(TagChip, { name: 'Briefe von der Front', color: 'sienna' });
+ const chip = document.querySelector('[data-testid="tag-chip"]') as HTMLElement;
+ expect(chip.getAttribute('title')).toBe('Briefe von der Front');
+ });
+
+ it('renders an HTML-bearing name as inert text, never markup (REQ-010)', () => {
+ const evil = '
';
+ render(TagChip, { name: evil, color: null });
+ expect(document.body.textContent).toContain(evil);
+ expect(document.querySelector('img')).toBeNull();
+ });
+});