scrollmirror
A vanilla JavaScript micro-library that syncs the scroll position of multiple elements on a page.
Repository Health
Technical Analysis
ScrollMirror is a lightweight, dependency-free JavaScript library that keeps the scroll position of multiple elements in sync. Point it at any NodeList or array of scrollable elements — including the page root — and it mirrors vertical and horizontal scroll as a percentage of each element’s own scrollable range, so panes of different lengths still line up proportionally instead of drifting apart.
Written in TypeScript and built with microbundle, it ships ESM, CommonJS, and UMD bundles alongside type declarations, so it drops into a bundler-based project or a plain <script> tag from a CDN with the same API. A small imperative surface — pause(), resume(), destroy(), and a settable progress property — covers the cases where mirrored panes need to be temporarily decoupled or driven manually.
What You Get
- A single dependency-free class (
ScrollMirror) with zero runtime dependencies - Proportional scroll mirroring driven by scroll-progress ratios rather than raw pixel offsets, so mismatched content lengths still line up
- Support for mirroring to/from the page root by including
:root,html, orbodyin the element selector - Prebuilt ESM, CommonJS, and UMD bundles plus bundled TypeScript type declarations
- An imperative API —
pause(),resume(),destroy(), and a gettable/settableprogressproperty — for manual control
Common Use Cases
- Side-by-side code diff or translation viewers where two panes must scroll together
- Synced comparison sliders (before/after images, pricing tables) that scroll in lockstep
- Custom scrollbars or minimap-style navigation that mirrors the main content’s scroll position
- Multi-column layouts (e.g. parallel document translations) that need proportional scroll sync despite differing content lengths
Under The Hood
Architecture
The library centers on a single ScrollMirror class (src/ScrollMirror.ts) that normalizes the provided elements to their nearest scroll container via getScrollContainer, deduplicates them, and attaches a shared handleScroll listener to each through addScrollHandler/removeScrollHandler in src/support/functions.ts. On scroll, handleScroll waits a tick (nextTick, using requestAnimationFrame) then computes the scrolled element’s progress with the pure helper getScrollProgress (src/support/helpers.ts) and calls mirrorScrollPositions, which applies that ratio to every other element via setScrollPosition while temporarily detaching and re-attaching each element’s own listener to prevent feedback loops. Pure scroll-math and validation logic is factored out of the class into helpers.ts and functions.ts, keeping the stateful class thin and the math independently testable.
Tech Stack
The package is authored entirely in TypeScript with no runtime dependencies, built via microbundle into modern ESM, legacy CJS, and a UMD bundle (targeted separately for <script>/CDN use) plus generated .d.ts declarations. Release automation runs on Changesets (@changesets/cli) with a dedicated version-or-publish GitHub Actions workflow, formatting is handled by Prettier, and the project ships its own documentation/demo site built with Astro (plus astro-expressive-code and astro-feather) deployed to Netlify.
Code Quality
The project has two separate, CI-enforced test suites: unit tests with Vitest (tests/unit, run in a jsdom environment, covering exports and internal helpers like the logger) and end-to-end tests with Playwright (tests/e2e) that drive the real demo page to assert actual scroll mirroring behavior across vertical, horizontal, both-axis, and root-mirroring scenarios. Both suites run in dedicated GitHub Actions workflows on every push. TypeScript provides type safety throughout, and JSDoc-style comments annotate public class members, though there is no dedicated ESLint config — Prettier is the only enforced style tool.
API Design
The public surface is intentionally small: construct with new ScrollMirror(elements, options), then call pause(), resume(), or destroy(), or read/write the progress property. The progress setter is notably ergonomic — it accepts a single number to set both axes at once, or a partial {x, y} object to set just one, falling back to the current value for the other. Debug mode adds console warnings (missing overflow, missing CSS overflow value) to catch common setup mistakes early, keeping the API approachable with minimal boilerplate to get a working mirror running.