Table of contents
The “On this page” rail: the headings of one document, on a left rule, with the one you are reading marked. It tracks scroll position rather than the URL hash, because those two disagree the moment anybody scrolls.
Anatomy
Code
<GdPageToc
:items="[
{ id: 'anatomy', label: 'Anatomy' },
{ id: 'api', label: 'API' }
]"
/> 11.5px items on a 2px left rule, the active one in accent. It is --gd-size-toc wide and carries no heading of its own — the chassis that places it supplies the “On this page” label, because a rail that titles itself cannot be reused anywhere the title is wrong.
How active is decided
- From SCROLL POSITION, never
location.hash. The hash only changes when somebody clicks a link, so scrolling past four headings leaves it pointing at the first — and the rail then says you are somewhere you are not. - Computed, not observed. An
IntersectionObserverneeds a detection band, and a band tight enough to pick exactly one heading is a band a short page can fall entirely outside. That is not hypothetical: it froze the rail on whichever heading last crossed it. - The rule is “the last heading whose top is above the reading line”. Unambiguous at every scroll position, including the two that break observers — the very top and the very bottom.
- It listens on the nearest SCROLLING ancestor, not the window. Inside an app frame the document does not scroll at all, so a window listener would never fire.
- Reads are throttled to a frame and the listener is passive, so it never fights the scroll it is measuring.
Never declare it twice
The items are DATA, and the temptation is to hand-write them beside the sections they describe. Do not: two lists with nothing holding them together drift the first time anybody edits a heading, and the failure is silent — a contents rail with a stale label or a dead anchor looks exactly like a working one.
The docs chassis reads them off the sections it is already rendering, so the rail cannot disagree with the page. Where a caller builds its own list, derive it from whatever already knows the headings rather than typing it out.
API
Props
items{ id: string; label: string }[]—The headings, in document order. Each `id` must match an element on the page — the rail scrolls to it and reads its position back. One list, not two: the docs chassis derives these from the sections it already renders rather than declaring them a second time.
For the other rail — the destination nav down the left of an app or a docs site — see Side navigation.