Anchor

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

Accessibility

Anchor renders plain links with no landmark, list or aria-current. The links work well, so most of the job is adding the structure around them.

What it renders

Anchor renders divs and links: a wrapper, a rail, a <span> for the moving bar, and one <div> per item holding an <a href="#id">. None of it carries ARIA.
  • It is not a landmark. Wrap it in <nav aria-label="On this page"> so screen reader users can jump to it or skip it. The label tells it apart from the page's other navigation.
  • It is not a list, so nothing announces how many links there are or which one the reader is on.
  • Nested items are read at the same level as their parent. The indent is visual only, another reason to nest one level at most.
  • The moving bar has no text and is ignored by screen readers.

The active item

The active link changes color and the bar moves next to it. No aria-current is set, and no prop adds one.
  • Screen readers are not told which section is current. Treat the rail as a visual aid, and make sure each heading says where the reader is.
  • To use the current section elsewhere, read it from onChange, which fires on every change from a scroll or a click.
  • The bar is hidden until a link is active, so a rail with nothing active shows no position at all. See the Usage tab on targetOffset and bounds.

Keyboard

The links are real links: each is a tab stop, Enter follows it, and the context menu can copy its address.
  • Every link is its own tab stop, so a rail of twenty links is twenty presses of Tab. Put the rail after the main content in the source, inside its <nav>, so readers can skip it.
  • Anchor handles the click and scrolls itself, so Ctrl-click or Cmd-click scrolls this page instead of opening a new tab.
  • onClick cannot stop the scroll. Calling preventDefault() there only skips the history entry.

Focus after a click

After a click, focus stays on the link in the rail while the page scrolls to the section. The next Tab continues from the rail, not from the section the reader jumped to.
  • Move focus yourself in onClick: give the target heading tabIndex={-1} and focus it. Let Anchor do the scrolling, without a second scrollIntoView.
  • Put tabIndex={-1} on the heading itself, not its wrapper, so the heading is read out when focus arrives. It needs no visible outline, since only code focuses it.

Motion and reduced motion

The bar slides with a CSS transition, and the page scrolls with an animation of its own. Only the first one can be turned off.
  • theme={{ token: { motion: false } }} stops the bar sliding, along with every other transition.
  • The scroll to a section always animates for about half a second. It ignores prefers-reduced-motion and scroll-behavior, and no prop changes it. If that is not acceptable for a page, use plain links instead of Anchor.
  • A horizontal anchor keeps its active link in view without an animation.

Contrast

Anchor uses three colors: colorText for links, colorPrimary for the active link and the bar, and colorSplit for the rail behind the bar.
  • Links use full-strength text color and pass by default. If you restyle them to a lighter color, check the contrast again.
  • The active link is colorPrimary on the page background. A primary chosen for white text on buttons can fail here.
  • The bar shows state, so it needs 3:1 contrast against the rail. It is only 2px wide, so a light primary makes it disappear.
  • Long titles are cut off with an ellipsis, and only string titles get a tooltip. Keep rail titles short.