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

minimal-scroll-sync

v0.0.5

Published

Tiny React scroll-sync hooks & emitter

Readme

Minimal Scroll Sync

CI npm bundlephobia

Demo of three panes scrolling in sync, along with a single parallax view

A lightweight, dependency‑free emitter + set of React hooks that keep any number of horizontally scrollable elements synchronized with minimal lag. Ideal for virtualized timelines, kanban boards, Gantt charts, parallax backgrounds, or anything that needs follow‑the‑leader scrolling.

Why another scroll‑sync lib? React is great for a lot of things, but sometimes it takes just a few too many milliseconds to pass a message between components.

Usually this tradeoff is worthwhile, but in the case of scrolling, it can lead to a 'rubber band' effect where the scroll position lags behind the mouse.

This library is designed to minimize that lag by using a simple event emitter to synchronize scroll positions between elements.


✨ Features

| Feature | Details | | ---------------------------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ⚡ Ultra‑light | Zero runtime deps. Just minzipped size minified + gzipped. | | 🪝 Hooks first | useScrollSync, useScrollSyncSubscribe, useScrollEndSubscribe for React apps. | | 🪢 Pure emitter | Non‑React projects can import scrollSyncEmitter directly. | | 🧠 Smart lead / follow logic | Only the actively‑scrolled element publishes; everyone else follows. | | 🏎️ RAF‑based | Updates happen in requestAnimationFrame, avoiding recursive scroll events. | | 🛑 Scroll‑end callback | Fire one final callback when scroll ends (for React state, analytics, etc.). |


🚀 Installation

npm i minimal-scroll-sync
# or
pnpm add minimal-scroll-sync

The package ships ESM, CJS, and type declarations—whatever your bundler prefers.


🏁 Quick start (three‑pane example)

import React, { useRef } from "react";
import { useScrollSync, useScrollEndSubscribe } from "minimal-scroll-sync";

export default function ScrollSyncDemo() {
  const leaderRef = useRef<HTMLDivElement>(null);
  const leaderRef2 = useRef<HTMLDivElement>(null);
  const followerRef = useRef<HTMLDivElement>(null);

  // Leader both publishes & follows (edge‑case safety)
  useScrollSync(leaderRef);
  // Leader also dispatches scroll end events out to state management
  useScrollEndSubscribe(leaderRef, offset => dispatch(setLeaderScrolledTo(offset)));

  // Leader 2 both publishes & follows (edge‑case safety)
  useScrollSync(leaderRef2);

  // Follower only follows; never publishes its own scroll
  // Example: If you have a parallax background, this is the one to use
  useScrollSyncSubscribe(followerRef);

  return (
    <>
      <div ref={leaderRef} className="pane scrollable">/* … */</div>
      <div ref={leaderRef2} className="pane scrollable">/* … */</div>
      <div ref={followerRef} className="pane scrollable">/* … */</div>
    </>
  );
}

That’s all; drag either of the first two panes, and the third glides perfectly alongside.


🔍 API

useScrollSync(ref)

Publishes and subscribes. Attach to every element that might lead the scroll. Under the hood it:

  1. Registers the element’s callback (scrollLeft = offset).
  2. Adds scroll and scrollend listeners that call scrollSyncEmitter.publish() / publishScrollEnd().

useScrollSyncSubscribe(ref, scrollSpeed = 1)

Read‑only follower. The optional scrollSpeed lets you build parallax effects by scrolling at a fraction (e.g. 0.25).

useScrollEndSubscribe(ref, cb)

Calls cb(offset) once when the user finishes interacting with ref (after a tiny debounce). Handy for scroll‑snap hacks, analytics pings, etc.

scrollSyncEmitter

If you’re outside React or writing your own wrapper you can rely on the bare emitter:

scrollSyncEmitter.subscribeScroll(element, cb);     // follow scrollLeft
scrollSyncEmitter.publish(element);                 // broadcast offset
scrollSyncEmitter.subscribeScrollEnd(element, cb);  // follow scrollend
scrollSyncEmitter.publishScrollEnd(element);        // broadcast + onEnd
scrollSyncEmitter.resetCurrentElement();            // manual reset (rare)

All subs return an unsubscribe function.


🧩 More examples

1. Virtualized timeline with auto‑snap

const parentRef = useRef<HTMLDivElement>(null);
useScrollSync(parentRef);
useScrollEndSubscribe(parentRef, offset => {
  const column = Math.round(offset / COLUMN_WIDTH);
  setDate(columns[column].date);            // Redux, Zustand, etc.
  dispatchSnapSemaphore();                  // trigger smooth snap
});

2. Parallax background follower


export default function ParallaxBackground() {
  const contentRef = useRef<HTMLDivElement>(null); // lead
  const bgRef      = useRef<HTMLDivElement>(null); // follow

  // Lead publishes scroll position
  useScrollSync(contentRef);
  // Follow subscribes to scroll position
  useScrollSyncSubscribe(bgRef, 0.25); // 4× slower parallax

  return (
    <div className="parallax">
      {/* Absolutely positioned in the background behind content */}
      <div ref={bgRef} className="bg" />
      <div ref={contentRef} className="content" />
    </div>
  );
}

🛠 Build & test

# compile ESM + CJS + d.ts
npm run build

# unit tests
npm run test         # one‑shot
npm run test:watch   # watch mode

🙌 Contributing

Issues and PRs are welcome! If you spot a race‑condition edge case, open an issue with a repro or failing test.

  1. git clone https://github.com/gilicaspi/minimal-scroll-sync
  2. pnpm install (or npm / yarn)
  3. Create a branch, add tests (npm run test), open PR.

We enforce prettier + eslint and green tests in CI.


💖 Sponsorship

If you find this library helpful and happen to know me personally, I accept sponsorship in the form of coffee, pizza, or other types of pizza.


📜 License

MIT © gilicaspi + contributors