npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

stacked-scroll

v1.0.2

Published

A small, hardened library that turns your sections into a stack of pages: each header stays pinned while you read, then hands off cleanly to the next page as it slides up from below.

Readme

stacked-scroll

A small, hardened library that turns your sections into a stack of pages: each header stays pinned while you read, then hands off cleanly to the next page as it slides up from below.

Built on top of GSAP ScrollTrigger.

Status

v1.0.0 — stable and tested. Used in production on rotyounges.com.

Install

npm install stacked-scroll gsap

gsap is a peer dependency.

Quick start

Your HTML is a stack of sections. Each section needs a header and a main content area.

<section class="panel" id="intro">
  <header class="header">Intro</header>
  <main class="main">…</main>
</section>

<section class="panel" id="work">
  <header class="header">Work</header>
  <main class="main">…</main>
</section>
import StackedScroll from 'stacked-scroll';

const scroller = new StackedScroll({
  sections: '.panel',
  headerSelector: '.header',
  mainSelector: '.main'
});

scroller.init();

// Deep-link to a section
scroller.scrollTo('work');

CSS requirements

The library only handles the JavaScript side of pinning. Your CSS must cooperate with GSAP:

  • Do not add global position: relative or z-index to your section elements — this breaks GSAP's pinning behavior.
  • Headers should use position: sticky; top: 0 if you want them to stay visible while the section content scrolls.
  • Use min-height: 100vh (or 100dvh) on sections so the pin-start logic can distinguish tall vs short panels.

See demo/index.html for a working example.

Hiding headers on short viewports

If you enable hideHeadersBelow, StackedScroll adds the configured class to <html> whenever the viewport is shorter than the threshold. You then decide what happens to the per-section header via CSS. Keep your site's primary navigation outside the section header so it remains accessible when the header is hidden.

/* Hide the per-section title bar on short viewports */
.stacked-scroll-compact .header {
  display: none;
}

/* Or collapse it instead of hiding it */
.stacked-scroll-compact .header {
  height: 2rem;
  padding: 0.25rem 1rem;
  font-size: 0.75rem;
}

Configuration

new StackedScroll({
  // Selector string, NodeList, array, or single HTMLElement
  sections: '.panel',

  // Selector for the sticky header inside each section (used for offset compensation)
  headerSelector: '.header',

  // Selector for the default scroll target inside each section
  mainSelector: '.main',

  // Optional: provide your own spacer element instead of the auto-created one.
  // The spacer must be outside all sections.
  spacerSelector: null,

  // Debounce window resize events
  resizeDebounceMs: 200,

  // Default scroll behavior for scrollTo()
  scrollBehavior: 'smooth',

  // Optional: when the viewport height (px) drops below this value, add
  // compactClassName to <html> so you can hide/collapse per-section headers.
  hideHeadersBelow: null,

  // Class added to <html> when the viewport is shorter than hideHeadersBelow.
  compactClassName: 'stacked-scroll-compact'
});

API

init()

Set up ScrollTriggers, the scroll-end spacer, and event listeners. Returns the instance.

scroller.init();

scrollTo(target, options)

Scroll to a section or to a specific element within a section.

// Scroll to a section by id
scroller.scrollTo('work');

// Scroll to a custom element inside the section
scroller.scrollTo('work', { offsetElement: '#work .lead' });

// Override the scroll behavior for this call
scroller.scrollTo('work', { behavior: 'auto' });

refresh()

Recalculate spacer sizing and refresh ScrollTrigger measurements. Call this after your content changes height.

scroller.refresh();

destroy()

Kill all ScrollTriggers, remove listeners, and remove the auto-created spacer.

scroller.destroy();

Why this exists

GSAP ScrollTrigger provides the low-level pin primitive, but packaging the specific "read the whole section, then hard-cut to the next" behavior involves several non-obvious edge cases:

  1. Deep links under pinned sectionsoffsetTop/offsetParent become unreliable when GSAP sets position: fixed on the active panel. scrollTo() avoids them entirely by summing natural panel heights and using a same-tick getBoundingClientRect() difference.
  2. Short last panel — when the last section is shorter than the viewport, the browser physically cannot scroll it flush to the top. stacked-scroll adds an invisible spacer sized to exactly the missing height.
  3. Final transition — when the last section is shorter than the viewport, the previous panel's pin end can land on the document's maximum scroll position, so GSAP can never scroll past it and the panel stays position: fixed. StackedScroll clamps every non-final pin end to just below maxScroll so the handoff always completes.
  4. Adjacent short panels — two consecutive sections both shorter than the viewport do not provide enough natural scroll distance for the previous pin to release before the next one engages. stacked-scroll injects a synthetic settle buffer between them so the handoff stays clean. The settle buffer fixes the scroll behavior, but a genuinely short panel (without min-height: 100vh) doesn't physically fill the viewport when pinned — the previous section's content will show through the gap. Keep your panels at min-height: 100vh and this never comes up.

Demo

npm install
npm run build
npx serve

Then open http://localhost:3000/demo/.

Tests

npm run test:e2e

Playwright regression tests cover the spacer fix and deep-link landing tolerance, including a fixture that reproduces the original pinned-section deep-link bug.

License

MIT