Pagefold

Scroll

Smooth scrolling between pages, to anchors and to the top.

Import it from pagefold/scroll (about 2.1 KB minified and compressed, shared code included).

import PagefoldScroll from 'pagefold/scroll'

new Pagefold({
  plugins: [
    new PagefoldScroll({
      duration: 800,
      offset: () => document.querySelector('header').offsetHeight,
      animateScroll: { betweenPages: false }, // jump between pages, glide to anchors
    }),
  ],
})

What it animates

KindExample
betweenPagesGoing to another page: scrolls to the top, or to the anchor if the URL has one
samePageWithHashA link to #section on the same page
samePageA link to the page you are already on, which scrolls to the top

Reset or restore

On every visit the page goes back to the top, as a browser would. On back and forward visits it goes to where you were when you left that page. Pagefold remembers the position of each page you leave, so this also works when animateHistoryBrowsing is on, which makes Pagefold, and not the browser, responsible for scrolling.

shouldResetScrollPosition changes this for links. A custom “back” link would normally scroll the new page to the top, while visitors expect it to behave like the back button:

new PagefoldScroll({
  shouldResetScrollPosition: (link) => !link.matches('.backlink'),
})

Scroll containers

An element with its own scrollbar, such as an overflowing table or a sidebar, is not part of the window’s scroll. Mark the ones that matter and Pagefold resets them on a visit and restores them on back:

<aside data-pagefold-scroll-container>...</aside>

Or choose them with the option: scrollContainers: '.sidebar, .results'. They are remembered by their order in the page, so a container that is replaced by a visit starts at the top anyway, and one that stays outside the containers is the one that benefits.

Marking the target

:target stops working on sites that add their own history entries. With markScrollTarget: true the element a hash points to gets the attribute data-pagefold-scroll-target, which you can style instead:

[data-pagefold-scroll-target] {
  outline: 2px solid var(--accent);
}

Hooks and the instance

scroll:start and scroll:end fire around an animated scroll. The plugin also adds pagefold.scrollTo(position, animate), the same as calling it on the plugin.

pagefold.hooks.on('scroll:start', () => document.body.classList.add('is-scrolling'))
pagefold.hooks.on('scroll:end', () => document.body.classList.remove('is-scrolling'))
await pagefold.scrollTo(2000, true)

Stopping

The animation stops as soon as the user takes over (wheel, touch, key press or mouse down) or a new visit starts.

Scrolling with your own function

To scroll with GSAP, or anything else, give the plugin a scrollFunction. It gets the position to reach and a context, and must call ctx.done() when it finishes. Return a function to stop it: Pagefold calls it when the user takes over or a new visit starts.

import { gsap } from 'gsap'
import { ScrollToPlugin } from 'gsap/ScrollToPlugin'
gsap.registerPlugin(ScrollToPlugin)

new PagefoldScroll({
  scrollFunction: (to, { duration, done }) => {
    const tween = gsap.to(window, {
      scrollTo: { y: to.top, x: to.left },
      duration: duration / 1000,
      ease: 'power2.inOut',
      onComplete: done,
    })
    return () => tween.kill()
  },
})

Jumps (animateScroll: false, reduced motion, or scrollTo(y, false)) do not use it.

Sideways

A page or container that scrolls horizontally can use offset: { left: 40 }. Without left, scrolling to an element never moves the page sideways.

From code

const scroll = new PagefoldScroll()
await scroll.scrollTo(0)                   // animated
await scroll.scrollTo(document.querySelector('#pricing'))
await scroll.scrollTo(300, false)          // jump

It replaces the default behavior of the scroll:top and scroll:anchor hooks. Without animateHistoryBrowsing, back and forward visits keep the browser’s own scroll restoration.

Common problems

  • The page jumps instead of gliding. Between pages the old content is usually hidden while it changes, so many sites turn betweenPages off. Check animateScroll.
  • A sticky header covers the target. Set offset to the header’s height, as in the example above.
  • Back and forward do not animate. They keep the browser’s own scroll restoration, on purpose.

Options

Option Type Default What it does
animateScroll boolean | object true true animates every kind. An object picks which: betweenPages, samePageWithHash, samePage.
duration number 600 Duration in milliseconds.
easing (t: number) => number easeInOutCubic Maps progress from 0 to 1 onto distance from 0 to 1.
offset number | { top, left } | (el) => number | { top, left } 0 Space to leave above the target, for example for a fixed header. { left } does the same sideways, for pages and containers that scroll horizontally.
scrollFunction (to, ctx) => void | (() => void) undefined Do the scrolling yourself, with GSAP for example. Call ctx.done() when it ends. Jumps and reduced motion do not use it.
scrollContainers string '[data-pagefold-scroll-container]' Elements other than the window whose scroll position is reset on a visit and restored on back.
shouldResetScrollPosition (link) => boolean () => true Return false to restore the position of the page a link goes to, instead of going to the top.
markScrollTarget boolean false Mark the element a hash points to with data-pagefold-scroll-target, to style it.
getAnchorElement (hash) => Element | null undefined Find the element a hash points to. Default: the element with that id, or a[name].
doScrollingRightAway boolean false Go to the top when the visit starts, not after the new content is in.
respectReducedMotion boolean true Jump instead of animating when the user prefers reduced motion.