Anchor To example

How it works

Smooth scroll to an id. Before it moves, it loads everything above the destination that can change the page height, so the anchor lands where it should.

AnchorTo scrolls the page to an element by its id, with easing and a fixed offset from the top. The scroll itself comes from the @terrahq/anchor-to package: a trigger, a destination, an offset. The trigger can be a button, a link or a <select>; the package listens to click on the first two and to change on the select.

The framework part is in AnchorTo.js. A slider, an accordion or a pinned section between the trigger and the destination is lazy loaded, so when the user clicks, it may not exist yet and the destination is measured too high. Before creating the package instance, the class reads src/js/resources.js, takes every library registered with modifyHeight: true and fires eventSystem.loadEvent for each. Every handler answers its own <Name>:load event by building all of its instances at once, so by the time the scroll starts the page already has its final height.

// AnchorTo.js: before scrolling, load every library registered with modifyHeight: true
loadHeightModifyingLibraries() {
  const libraries = getModules();
  const heightModifyingLibraries = libraries.filter((lib) => lib.options?.modifyHeight);
  heightModifyingLibraries.map((lib) => {
    this.eventSystem.loadEvent({ library: lib.name, where: "AnchorTo" });
  });
}

The markup

A trigger with the js--anchor-to hook and an anchor-id attribute, which is the id of the destination. It is a plain attribute, not data-anchor-id. For a select, the hook is js--anchor-select and every option value is an id; no attribute needed.

<!-- The trigger: anchor-id is the id of the destination, no data- prefix -->
<button class="c--btn-a js--anchor-to" anchor-id="destination">Go there</button>

<!-- Or a select: every option value is the id of a destination -->
<select class="c--form-select-a__item js--anchor-select">
  <option value="destination">Destination</option>
</select>

<!-- The destination, anywhere below or above -->
<h2 id="destination">Destination</h2>

The handler

Two configs in Handler.js, one per hook. The button one resolves the destination from anchor-id once, when the instance is built; the select one leaves it out, since the package reads the id from the chosen option on every change. Both use an offset of 120px and never touch the URL:

this.config = ({element}) => {
    return {
        trigger: element,
        destination: document.getElementById(element.getAttribute('anchor-id')),
        offset: 120,
        url: "none",
        eventSystem: this.eventSystem,
    };
};
this.selectConfig = ({element}) => {
    return {
        trigger: element,
        offset: 120,
        url: "none",
        eventSystem: this.eventSystem,
    };
};

What the framework does for you

  • Lazy loading. A trigger is only wired when it gets within 30px of the viewport (Boostify scroll). The package is imported once and cached by the Manager.
  • Page transitions. Every instance is destroyed before swup replaces the content and rebuilt on the new page.
  • Cross-handler events. AnchorTo fires loadEvent for every modifyHeight library: Collapsify, Slider, SliderKit, Lottie, ToggleGallery, PinnedScroll, StickyScroll. The chapter strip under the hero of every docs page is a list of these anchors.

Button

A button with an anchor-id. The slider and the accordion in between load first, then the page scrolls.

Default anchor-id="destination"offset: 120url: "none"

The common case. The button carries anchor-id="destination" and the heading at the bottom of the live example has that id. Click it: the page eases down and stops 120px above the heading.

Between the two there is tall content on purpose: a slider and an accordion, both lazy loaded and both registered with modifyHeight: true. Without the load step they would still be empty boxes when the scroll starts and the heading would end up out of view. With it, AnchorTo builds them first and the destination is measured at its final place.

<button class="c--btn-a js--anchor-to" anchor-id="destination">Anchor button</button>

<!-- Tall content in between: a slider and an accordion, both registered with modifyHeight -->
<div class="c--slider-a js--slider">…</div>
<div class="c--accordion-a js--collapsify-accordion">…</div>

<h2 id="destination">Anchor To destination</h2>
Live example
anchor-id="destination"offset: 120url: "none"
alt text alt text
1
2
3
4
5
6

Second content : Lorem ipsum, dolor sit amet consectetur adipisicing elit. Fugit repellat optio ab fugiat pariatur eius dolores omnis ullam nostrum ad vitae praesentium ex veniam veritatis consequatur, quidem consectetur nemo? Voluptatibus.

Anchor To destination

alt text alt text

Select

A select whose options are ids. Change it and the page scrolls to the chosen one.

Variation js--anchor-selectoffset: 120url: "none"

Same package, other trigger. With a <select> the package listens to change instead of click and takes the destination from the value of the chosen option, so one select can point to as many sections as it has options. The config has no destination: there is nothing to resolve until the user picks one.

<div class="c--form-group-a">
  <label class="c--label-a" for="goto">Go To</label>
  <div class="c--form-select-a">
    <select id="goto" class="c--form-select-a__item js--anchor-select">
      <option value="a">a</option>
      <option value="destination">Destination</option>
      <option value="b">b</option>
    </select>
  </div>
</div>
this.selectConfig = ({element}) => {
    return {
        trigger: element,
        offset: 120,
        url: "none",
        eventSystem: this.eventSystem,
    };
};

Options reference

What the HTML carries, what Handler.js sets, and what the package accepts.

Reference

HTML

AttributeValuesWhat it does
anchor-idthe id of an element, on a js--anchor-to triggerWhere the page scrolls to. Plain attribute, no data- prefix
valuethe id of an element, on each option of a js--anchor-selectWhere the page scrolls to when that option is chosen

Config (Handler.js, passed to the package)

OptionDefaultWhat it does
triggerrequiredThe element that starts the scroll. A <select> fires on change, anything else on click
destinationrequired for click triggersThe element to scroll to. Handler.js resolves it from anchor-id; a select sets it on every change
offset0 (Handler.js sets 120)Pixels left above the destination. Can be a function (destination, trigger) => number, for a sticky header measured on the fly
url"hash" (Handler.js sets "none")"hash" writes #id to the URL, "query" writes ?scrollto=id, "none" leaves it alone
speed1500Duration of the scroll, in milliseconds
emitEventstrueDispatches AnchorToStart and AnchorToEnd on the trigger
popstatetrueScrolls to the id in the URL on back and forward navigation. Only useful with url set to "hash" or "query"
onCompletenullCalled when the scroll ends
microAdjusttrueAfter the scroll, corrects the position if it is off by more than microAdjustThreshold (6px) in microAdjustDuration (150ms)
postSettleAdjusttrueWatches the page height for up to postSettleMaxWait (1000ms) after the scroll and corrects the position if content loads late. Select triggers only
disableCssSmoothDuringScrolltrueTurns off CSS scroll-behavior: smooth while the animation runs, so the two do not fight
debugfalseLogs the resolved config to the console

eventSystem in the Handler.js configs is not a package option: AnchorTo.js keeps it to fire the load events and passes the rest on. To make a library take part in that step, register it in resources.js with options: { modifyHeight: true } and answer its <Name>:load event in its handler.