---
id: S04
parent: M016
milestone: M016
provides:
  - IntersectionObserver scroll-spy pattern in TableOfContents.tsx — S05 sticky reading header can reuse the same activeId approach
  - Sidebar ToC positioning — sticky sidebar layout confirmed working
requires:
  []
affects:
  - S05
key_files:
  - frontend/src/pages/TechniquePage.tsx
  - frontend/src/components/TableOfContents.tsx
  - frontend/src/App.css
key_decisions:
  - ToC only renders in sidebar for v2-format pages; v1 pages have no ToC
  - Active styles use --color-accent-focus (15% opacity) vs hover's --color-accent-subtle (10% opacity) plus font-weight 500
  - rootMargin 0px 0px -70% 0px triggers active state when section enters top 30% of viewport
patterns_established:
  - IntersectionObserver scroll-spy pattern with rootMargin for predictive active section tracking — reusable for S05 sticky reading header
observability_surfaces:
  - none
drill_down_paths:
  - .gsd/milestones/M016/slices/S04/tasks/T01-SUMMARY.md
  - .gsd/milestones/M016/slices/S04/tasks/T02-SUMMARY.md
duration: ""
verification_result: passed
completed_at: 2026-04-03T05:55:24.264Z
blocker_discovered: false
---

# S04: ToC Modernization

**Modernized technique page Table of Contents: moved to sidebar, replaced counters with accent bar, added scroll-spy active section highlighting via IntersectionObserver.**

## What Happened

Two tasks delivered the full ToC modernization for technique pages.

T01 relocated the TableOfContents component from inside the v2 prose column to the top of the sidebar div, making it automatically sticky (sidebar already has position: sticky). Restyled from a card with CSS counters to a clean list with a 2px left accent bar (--color-accent), hover backgrounds (--color-accent-subtle), and an "On this page" heading. Changed ordered list to unordered list. Gated rendering to v2-format pages only since v1 pages lack structured section hierarchy.

T02 added IntersectionObserver-based scroll-spy. A useEffect creates an observer watching all section/subsection heading elements (H2 slugs and H3 compound slugs with -- separator). rootMargin of '0px 0px -70% 0px' means a section becomes active when it enters the top 30% of the viewport. Active state uses --color-accent-focus (15% opacity) background and font-weight 500, visually distinct from the hover state (10% opacity, normal weight). Observer disconnects on unmount.

## Verification

Frontend production build passes with zero errors and zero warnings (npm run build, 945ms). All three key files modified: TechniquePage.tsx (ToC moved to sidebar), TableOfContents.tsx (new heading, ul, IntersectionObserver scroll-spy), App.css (accent bar, hover/active styles). Code inspection confirms: activeId state, IntersectionObserver with rootMargin, conditional active class names, CSS rules for --active variants.

## Requirements Advanced

- R040 — All acceptance criteria delivered: no counters, left accent bar, On this page heading, hover states, IntersectionObserver active highlighting, sticky in sidebar via existing sidebar positioning.

## Requirements Validated

None.

## New Requirements Surfaced

None.

## Requirements Invalidated or Re-scoped

None.

## Deviations

T02 added a non-null assertion on intersecting[0] after a length > 0 guard because TypeScript couldn't narrow through the .sort() chain. Minor, no functional impact.

## Known Limitations

None.

## Follow-ups

None.

## Files Created/Modified

- `frontend/src/pages/TechniquePage.tsx` — Moved TableOfContents render from v2 prose column to sidebar top, gated on v2 format
- `frontend/src/components/TableOfContents.tsx` — New heading 'On this page', ol→ul, IntersectionObserver scroll-spy with activeId state and conditional active class names
- `frontend/src/App.css` — Replaced counter/card ToC styles with accent bar, hover backgrounds, and active state styles (--active variants)
