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

scrollbooster

v4.0.0

Published

Enjoyable content drag-to-scroll library

Readme

ScrollBooster

Enjoyable drag-to-scroll micro library (~6 kB gzipped). Supports smooth content scroll via mouse, touch and pen dragging, trackpad or mouse wheel, with inertia and elastic bounce. Zero dependencies.

Easy to setup yet flexible enough to support any custom scrolling logic.

Upgrading from 3.x? See MIGRATION.md.

Installation

npm i scrollbooster

Or with <script> tag, the library is available as global ScrollBooster:

<script src="https://unpkg.com/scrollbooster@4/dist/scrollbooster.min.js"></script>

Usage

Simplest setup:

import { ScrollBooster } from 'scrollbooster';

new ScrollBooster({
    viewport: document.querySelector('.viewport'),
});

The package is ESM only, require() loads it in Node 20.19+ and 22.12+:

const { ScrollBooster } = require('scrollbooster');

Types for options and state are exported for TypeScript:

import { ScrollBooster, type ScrollBoosterOptions, type ScrollBoosterState } from 'scrollbooster';

Options

Option | Type | Default | Description ------ | ---- | ------- | ----------- viewport | HTMLElement | | Element that shows and clips the content (required) content | HTMLElement | first child of viewport | Element that moves inside viewport direction | 'all', 'horizontal', 'vertical' | 'all' | Directions the content scrolls in scrollMode | 'transform', 'native', 'none' | 'transform' | How the position is rendered, see Choosing scrollMode pointerMode | 'all', 'touch', 'mouse' | 'all' | Pointers that drag content, 'mouse' includes pen bounce | boolean | true | Content can be pulled beyond the edges with resistance and bounces back when thrown beyond them friction | number | 0.05 | How fast motion slows down after release, between 0 and 1 bounceForce | number | 0.1 | How fast content returns from beyond an edge, between 0 and 1 textSelection | boolean | false | Text can be selected: a mouse press on text selects it instead of dragging, a long touch press selects a word axisLock | boolean | false | A drag or a wheel gesture moves content only along the axis it starts along, for direction: 'all' inputsFocus | boolean | true | A press on a form control, summary, media controls or editable content, or inside them, does not drag wheel | true, false, 'horizontal' | true | Wheel and trackpad scroll content. The page scrolls instead when content cannot move that way. 'horizontal' scrolls only along x, a vertical mouse wheel too keyboard | boolean | true | Keys scroll content while it has focus, see Accessibility reducedMotion | 'auto', 'always', 'never' | 'auto' | 'always' turns off inertia, bounce and smooth scrollTo, 'never' keeps them, 'auto' follows the prefers-reduced-motion setting snap | function | | Where content stops after a drag or a wheel gesture, for carousels and paging. Receives the position where it would stop and the state, returns the position to scroll to, for example (rest) => ({ x: Math.round(rest.x / 300) * 300 }) shouldDrag | function | | Return false to not drag from this press, for example on a slider inside content. Receives the state and the PointerEvent onUpdate | function | | Called with the state on animation frames while anything changes, the first time after creation onClick | function | | Called with the state and the event for each click in viewport. event.defaultPrevented is true for the click that ends a drag onPointerDown | function | | Called with the state and the PointerEvent when a press starts dragging onPointerMove | function | | Called when the dragging pointer moves onPointerUp | function | | Called when the dragging pointer is released or cancelled onWheel | function | | Called for each wheel event that scrolls content

friction and bounceForce are per 60 Hz frame, motion is the same on any refresh rate.

On touch screens ScrollBooster sets CSS touch-action on the viewport, so swipes across direction scroll the page and pinch zoom always works.

Methods

Method | Description ------ | ----------- scrollTo | Smooth scroll to { x, y }, a missing coordinate stays. Does nothing while the user drags content scrollBy | Smooth scroll by { x, y }. Repeated calls add up, for "next" and "previous" buttons scrollIntoView | Smooth scroll to show an element of the content: sb.scrollIntoView(element, { align }), align is 'nearest' (default), 'start', 'center' or 'end' setPosition | Jump to { x, y } and stop motion getState | Current state, the same as onUpdate receives updateOptions | Change options, including viewport and content updateMetrics | Measure viewport and content again. Size changes are tracked automatically, call it for other changes, for example of direction: rtl destroy | Stop motion, remove listeners and the styles the instance set

Positions stay within the edges. State has position, maxPosition, viewport and content sizes, isMoving, isDragging, dragOffset, dragAngle and borderCollision.

Example

const sb = new ScrollBooster({
  viewport: document.querySelector('.gallery'),
  direction: 'horizontal',
  onUpdate: (state) => {
    progress.value = state.position.x / (state.maxPosition.x || 1);
  },
  onClick: (state, event) => {
    if (!event.defaultPrevented) {
      // a click without drag
    }
  },
});

sb.scrollTo({ x: 0 });

Choosing scrollMode

  • 'transform' (default) moves content with the CSS translate property and bounces beyond the edges. Your own transform of the content, like scale(), stays. The viewport itself does not scroll, so position: sticky inside content and code that listens to scroll do not work. Use sb.scrollIntoView() instead of element.scrollIntoView().
  • 'native' sets scrollLeft and scrollTop of the viewport. No bounce, everything the browser does with a scroller works.
  • 'none' renders nothing, render the position in onUpdate. Set will-change: transform on the content in CSS, so it is not repainted every frame:
onUpdate: ({ position }) => {
  // in a right-to-left viewport use +position.x
  content.style.transform = `translate(${-position.x}px, ${-position.y}px)`;
},

Recipes

Gallery with links: clicks work, the click that ends a drag does not reach links and buttons.

new ScrollBooster({ viewport, direction: 'horizontal' });

Carousel that stops on a slide, with buttons:

const slide = 300;
const sb = new ScrollBooster({
  viewport,
  direction: 'horizontal',
  snap: (rest) => ({ x: Math.round(rest.x / slide) * slide }),
});
next.addEventListener('click', () => sb.scrollBy({ x: slide }));
prev.addEventListener('click', () => sb.scrollBy({ x: -slide }));

Show the active tab: sb.scrollIntoView(activeTab, { align: 'center' }).

Drag with the mouse on desktop, native scroll on touch screens:

// .viewport { overflow: auto; }
new ScrollBooster({ viewport, pointerMode: 'mouse', scrollMode: 'native' });

Drag the whole page with the mouse:

new ScrollBooster({
  viewport: document.documentElement,
  content: document.body,
  scrollMode: 'native',
  direction: 'vertical',
  pointerMode: 'mouse',
});

Run code when motion ends, the last onUpdate of a motion has isMoving: false:

let wasMoving = false;
new ScrollBooster({
  viewport,
  onUpdate: (state) => {
    if (wasMoving && !state.isMoving) {
      loadMoreIfNeeded(state.position, state.maxPosition);
    }
    wasMoving = state.isMoving;
  },
});

React:

function Gallery({ children, direction }) {
  const viewport = useRef(null);
  const sb = useRef(null);
  useEffect(() => {
    sb.current = new ScrollBooster({ viewport: viewport.current, direction });
    return () => sb.current.destroy();
  }, []);
  useEffect(() => sb.current.updateOptions({ direction }), [direction]);
  return (
    <div ref={viewport} style={{ overflow: 'hidden' }}>
      <div style={{ display: 'flex', width: 'max-content' }}>{children}</div>
    </div>
  );
}

Tests in jsdom: there is no ResizeObserver, call updateMetrics() after changing sizes. Jest in CommonJS mode has to transform the package: transformIgnorePatterns: ['/node_modules/(?!scrollbooster)'].

Accessibility

  • With prefers-reduced-motion: reduce content stops right after release, does not bounce and scrollTo jumps to the target, see the reducedMotion option.
  • Focus on an element outside the visible area, for example with Tab, scrolls content to show it.
  • Arrow keys, Page Up, Page Down, Space, Home and End scroll content while the viewport or an element in it has focus, like a native scroller. Keys in inputs are left alone. Make the viewport focusable when its content has no focusable elements: <section class="viewport" tabindex="0" aria-label="Gallery">.

Nested instances

An instance may live inside the content of another one, for example horizontal rows in a vertical board. Like native scroll, a gesture goes to the innermost instance that can move along it, a row at its edge passes it to the outer content.

Right-to-left

In a viewport with direction: rtl content starts at the right edge, and position.x is the distance from it: it grows to the left, from 0 to maxPosition.x, so progress bars and scrollTo() work the same in both directions. dragOffset and borderCollision stay physical, borderCollision.right is true at the start.

Browser support

Chrome and Edge 104+, Firefox 90+, Safari and iOS Safari 15+. Version 3.x supports IE11.

Special thanks

David DeSandro for his talk "Practical UI Physics".

License

MIT License (c) Ilya Shubin