Anchor

A table of contents for one page, whose active link follows the reader as they scroll.

Usage

When to use Anchor, which element it should listen to, and when a section counts as the current one.

When to use

Anchor is a table of contents for one page: a list of links to its sections, with the section in view highlighted as the reader scrolls.
Use it when the page is long enough that readers lose their place, and every link goes to a heading on that page. Use something else when:
  • The links go to other pages. Use Menu. Anchor treats every href as an id on the current page, so a link elsewhere never scrolls or lights up, and nothing tells you.
  • The sections are not all on the page at once. Use Tabs. Anchor can only track sections that are rendered.
  • The page fits in a screen or two. A rail of three links takes a column and saves no scrolling.

The items list

Build the rail from items, not from <Anchor.Link> children. Children are deprecated and log a warning.
  • Each item has a key, an href and a title. The href is # plus the section's id, and must match it exactly. Check that first when a link does nothing.
  • Nest with children, one level deep. Deeper levels render with the same indent, and a three-level rail is hard to read.
  • title takes any node, but only a string title gets a native tooltip. Long titles are cut off with an ellipsis, so keep node titles short.
  • An item can set its own targetOffset and replace, which win over the ones on Anchor.

affix and sticky rails

affix is on by default. It switches the rail to position: fixed once it scrolls past its offset. Inside a wrapper that is already sticky, this lets the rail escape its column and float over the page.
  • Let one thing position the rail: either affix with offsetTop, or affix={false} inside a sticky wrapper of your own.
  • affix also takes an object of Affix props, apart from offsetTop, target and children.
  • The rail sets its own max height of the viewport minus offsetTop. Inside your own scrolling box, pass style={{ maxHeight: "none" }} and let the box clip it.

getContainer

getContainer is the element Anchor listens to and measures from. It defaults to the window.
  • When the sections scroll inside a panel, pass a function that returns the panel. Otherwise Anchor listens to a window that is not scrolling, and the highlight never moves.
  • The offsets below are measured from the top of this element, not the viewport.
  • Keep the container the same for the life of the Anchor. Swapping it after the first render does not move the listener.

targetOffset and bounds

targetOffset sets two things: where a clicked section lands (that many pixels below the container's top), and when a section becomes active (once its top is within targetOffset plus bounds, 5px by default, of the top).
  • Under a sticky header, set targetOffset to the header's height. Without it, a section becomes active only once its heading is already hidden behind the header.
  • If the container has padding above the first heading, no link is active until the reader scrolls a little. Raise bounds to cover the padding.
  • Leave room below the last section. It becomes active only when its top reaches the top of the container, so if the content ends first, its link never lights up.
  • offsetTop sets where an affixed rail pins, and is also used in place of targetOffset when that is not set. Set both when the two should differ.
  • For a rule that is not a distance, use getCurrentAnchor. It receives the link Anchor would highlight and returns the one to highlight instead, like a chapter's part.

direction

direction takes vertical (the default) or horizontal.
  • A horizontal anchor takes no nested items. It logs a warning and drops them, so a nested rail switched to horizontal loses links.
  • A horizontal anchor scrolls sideways and keeps the active link in view. A vertical rail does not scroll: a list taller than its column is cut off.
  • A horizontal anchor fits on one line, so keep to about five short titles.

replace and history

By default each click adds the section to the browser history, so Back steps through every section the reader visited before it leaves the page.
  • Set replace for a table of contents. Each click then replaces the current entry, and Back goes to the previous page.
  • An href starting with http:// or https:// is left to the browser. With replace, it replaces the current page instead.
  • Ctrl-click or Cmd-click on a link scrolls this page instead of opening a new tab.
  • onChange fires with the link in view, from a scroll or a click, before getCurrentAnchor changes it.
  • preventDefault() in onClick skips the history entry but still scrolls. That suits a page whose URL a router owns.

Build the list from the sections

A rail written by hand next to headings written by hand drifts apart, and Anchor says nothing: a link whose id is gone never lights up, and clicking it goes nowhere.
  • Keep one array that renders both the headings and the items, so a new section cannot leave a dead link.
  • When the headings come from content, make the ids with the same function on both sides, in the same pass.
  • Treat the ids as part of your API. Other pages link to them, so rename them with care.