diff --git a/frontend/src/routes/admin/tags/[id]/TagAncestry.svelte b/frontend/src/routes/admin/tags/[id]/TagAncestry.svelte
new file mode 100644
index 00000000..81453b05
--- /dev/null
+++ b/frontend/src/routes/admin/tags/[id]/TagAncestry.svelte
@@ -0,0 +1,43 @@
+
+
+{#if ancestors.length > 0}
+
+{/if}
diff --git a/frontend/src/routes/admin/tags/[id]/TagAncestry.svelte.spec.ts b/frontend/src/routes/admin/tags/[id]/TagAncestry.svelte.spec.ts
new file mode 100644
index 00000000..5f3f72b7
--- /dev/null
+++ b/frontend/src/routes/admin/tags/[id]/TagAncestry.svelte.spec.ts
@@ -0,0 +1,60 @@
+import { afterEach, describe, expect, it } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import { page } from 'vitest/browser';
+import TagAncestry from './TagAncestry.svelte';
+
+afterEach(cleanup);
+
+const allTags = [
+ { id: 't1', name: 'Root', documentCount: 0 },
+ { id: 't2', name: 'Child', parentId: 't1', documentCount: 0 },
+ { id: 't3', name: 'Grandchild', parentId: 't2', documentCount: 0 }
+];
+
+describe('TagAncestry', () => {
+ it('renders nothing for a root tag', async () => {
+ const { container } = render(TagAncestry, {
+ tag: { id: 't1', name: 'Root', documentCount: 0 },
+ allTags
+ });
+ expect(container.querySelector('nav')).toBeFalsy();
+ });
+
+ it('renders a nav element for a child tag', async () => {
+ const { container } = render(TagAncestry, {
+ tag: { id: 't2', name: 'Child', parentId: 't1', documentCount: 0 },
+ allTags
+ });
+ expect(container.querySelector('nav')).toBeTruthy();
+ });
+
+ it('shows parent name as a link', async () => {
+ render(TagAncestry, {
+ tag: { id: 't2', name: 'Child', parentId: 't1', documentCount: 0 },
+ allTags
+ });
+ await expect.element(page.getByRole('link', { name: 'Root' })).toBeInTheDocument();
+ await expect
+ .element(page.getByRole('link', { name: 'Root' }))
+ .toHaveAttribute('href', '/admin/tags/t1');
+ });
+
+ it('shows full ancestor chain for deeply nested tag', async () => {
+ render(TagAncestry, {
+ tag: { id: 't3', name: 'Grandchild', parentId: 't2', documentCount: 0 },
+ allTags
+ });
+ await expect.element(page.getByRole('link', { name: 'Root' })).toBeInTheDocument();
+ await expect.element(page.getByRole('link', { name: 'Child' })).toBeInTheDocument();
+ });
+
+ it('does not render current tag as a link', async () => {
+ render(TagAncestry, {
+ tag: { id: 't2', name: 'Child', parentId: 't1', documentCount: 0 },
+ allTags
+ });
+ const links = document.querySelectorAll('nav a');
+ const linkTexts = Array.from(links).map((a) => a.textContent?.trim());
+ expect(linkTexts).not.toContain('Child');
+ });
+});
diff --git a/frontend/src/routes/admin/tags/[id]/TagChildrenPreview.svelte b/frontend/src/routes/admin/tags/[id]/TagChildrenPreview.svelte
new file mode 100644
index 00000000..8df5c1bc
--- /dev/null
+++ b/frontend/src/routes/admin/tags/[id]/TagChildrenPreview.svelte
@@ -0,0 +1,52 @@
+
+
+{#if children.length > 0}
+
+
+ {m.admin_tag_children_label()}
+
+
+ {#each visibleChildren as child (child.id)}
+
{child.name}
+ {/each}
+ {#if !showAll && children.length > 5}
+
+ {/if}
+
+
+{/if}
diff --git a/frontend/src/routes/admin/tags/[id]/TagChildrenPreview.svelte.spec.ts b/frontend/src/routes/admin/tags/[id]/TagChildrenPreview.svelte.spec.ts
new file mode 100644
index 00000000..49cb0708
--- /dev/null
+++ b/frontend/src/routes/admin/tags/[id]/TagChildrenPreview.svelte.spec.ts
@@ -0,0 +1,75 @@
+import { afterEach, describe, expect, it } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import { page } from 'vitest/browser';
+import TagChildrenPreview from './TagChildrenPreview.svelte';
+
+afterEach(cleanup);
+
+const allTags = [
+ { id: 't1', name: 'Root', documentCount: 0 },
+ { id: 't2', name: 'Alpha', parentId: 't1', documentCount: 0 },
+ { id: 't3', name: 'Beta', parentId: 't1', documentCount: 0 },
+ { id: 't4', name: 'Gamma', parentId: 't1', documentCount: 0 },
+ { id: 't5', name: 'Delta', parentId: 't1', documentCount: 0 },
+ { id: 't6', name: 'Epsilon', parentId: 't1', documentCount: 0 },
+ { id: 't7', name: 'Zeta', parentId: 't1', documentCount: 0 }
+];
+
+describe('TagChildrenPreview', () => {
+ it('renders nothing for a leaf tag', async () => {
+ const { container } = render(TagChildrenPreview, {
+ tag: { id: 't2', name: 'Alpha', parentId: 't1', documentCount: 0 },
+ allTags
+ });
+ expect(container.querySelector('[data-testid="children-preview"]')).toBeFalsy();
+ });
+
+ it('renders section heading for a tag with children', async () => {
+ render(TagChildrenPreview, {
+ tag: { id: 't1', name: 'Root', documentCount: 0 },
+ allTags
+ });
+ await expect.element(page.getByTestId('children-preview')).toBeInTheDocument();
+ });
+
+ it('shows up to 5 children as chips', async () => {
+ render(TagChildrenPreview, {
+ tag: { id: 't1', name: 'Root', documentCount: 0 },
+ allTags
+ });
+ // Alpha through Epsilon should be visible (5 chips)
+ await expect.element(page.getByText('Alpha')).toBeInTheDocument();
+ await expect.element(page.getByText('Epsilon')).toBeInTheDocument();
+ });
+
+ it('shows expand button when there are more than 5 children', async () => {
+ render(TagChildrenPreview, {
+ tag: { id: 't1', name: 'Root', documentCount: 0 },
+ allTags
+ });
+ // There are 6 children — should show "und 1 weitere" expand button
+ const expandBtn = document.querySelector(
+ 'button[data-testid="expand-children"]'
+ );
+ expect(expandBtn).toBeTruthy();
+ });
+
+ it('expand button reveals hidden children inline', async () => {
+ render(TagChildrenPreview, {
+ tag: { id: 't1', name: 'Root', documentCount: 0 },
+ allTags
+ });
+ // Zeta is hidden behind the expand button
+ await expect.element(page.getByText('Zeta')).not.toBeInTheDocument();
+ await page.getByTestId('expand-children').click();
+ await expect.element(page.getByText('Zeta')).toBeInTheDocument();
+ });
+
+ it('does not show expand button when children count <= 5', async () => {
+ render(TagChildrenPreview, {
+ tag: { id: 't1', name: 'Root', documentCount: 0 },
+ allTags: allTags.slice(0, 4) // Root + 3 children
+ });
+ expect(document.querySelector('[data-testid="expand-children"]')).toBeFalsy();
+ });
+});