# scrollbooster makes a container draggable without taking over the scrollbar

> scrollbooster is a small, dependency free library that turns any element into a drag to scroll surface, with inertia, elastic bounce and snapping, and it lets you choose how the position is rendered: a CSS transform, the native scroll position, or nothing at all. The option table is unusually well specified, and the three rendering modes are the decision that really matters.

**ilyashubin/scrollbooster** — Enjoyable content drag-to-scroll library

- Repository: https://github.com/ilyashubin/scrollbooster
- Website: https://ilyashubin.github.io/scrollbooster
- Stars: 1,005 · Forks: 78
- Language: TypeScript
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/ilyashubin-scrollbooster

## Three ways to render the position, and they are not interchangeable

The `scrollMode` option is the centre of the library, because it decides what the browser thinks it is looking at. In the default `transform` mode the content is moved with the CSS `translate` property, and it can be pulled past the edges and bounce back. The trade is that the viewport itself never scrolls, which means `position: sticky` inside the content stops working and any code that listens for `scroll` events hears nothing, so the documentation tells you to use `sb.scrollIntoView()` rather than `element.scrollIntoView()`. In `native` mode the library sets `scrollLeft` and `scrollTop` on the viewport instead, which gives up the bounce but restores everything the browser does with a scroller. The third mode, `none`, renders nothing at all and hands you the position in `onUpdate` so you can position the content yourself, with the documentation suggesting that you set `will-change: transform` in CSS so the browser does not repaint it every frame.

## Setting up a drag surface takes one call

The quickest way to see the library work is to give it a viewport and nothing else. Install it from npm:

```bash
npm i scrollbooster
```

Then construct it with the element that clips the content:

```js
import { ScrollBooster } from 'scrollbooster';

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

The viewport is the only required option, and the content element defaults to the first child of the viewport, so a container with one inner element is enough to get started. The live examples page in the repository shows the same thing done with a horizontal gallery, with a nested scroll area, with the native mode, and with a custom `onUpdate` that positions the content by hand, each one with its code alongside. A gallery that has to sit inside a page that also scrolls is the case the touch handling was designed for, since the library sets `touch-action` on the viewport rather than capturing every gesture.

## The options are the design, and they are specified tightly

The option table is long, and reading it tells you how much thought went into the feel of the thing. Motion is defined by `friction`, the rate at which motion slows after release, with a default of 0.05, and `bounceForce`, the rate at which content returns from beyond an edge, with a default of 0.1. Both are per 60 hertz frame, and the documentation is explicit that the motion is the same on any refresh rate, which is a stronger claim than most libraries make. Interaction is controlled by `pointerMode`, where `all` covers every pointer, `mouse` includes pen, and `touch` excludes them, by `textSelection`, which is off by default so a press on text drags instead of selecting, and by `inputsFocus`, on by default, which stops a drag from starting on a form control, a `summary` element, media controls or editable content. `axisLock` keeps a gesture on the axis it started on, and `reducedMotion` has three settings, with `auto` following the `prefers-reduced-motion` media query, `always` turning off inertia, bounce and smooth scrolling, and `never` keeping them.

## Wheel, keyboard and touch all end up at the same content

Dragging is the headline, but the library is careful about the other input paths. The `wheel` option controls both the mouse wheel and trackpad, and it is not a boolean in practice: it can be `true`, `false` or `'horizontal'`, where the last one scrolls only along the x axis and a vertical mouse wheel still works. The behaviour when the content cannot move further is stated plainly, the page scrolls instead, which is what keeps the library from trapping a user at the end of a list. Keyboard scrolling is on by default and works while the content has focus, and that is where the accessibility story lives. On touch screens the library sets the CSS `touch-action` property on the viewport, so a swipe across the allowed direction scrolls the page while a pinch zoom always works, which is the detail that usually breaks a custom drag implementation on a phone.

## Callbacks hand you the state on every frame

The callback set is what turns the library from a widget into a component. `onUpdate` is called with the state on animation frames while anything changes, and also once right after creation, so a progress bar or a scroll indicator can be driven entirely from the library's own numbers. `onClick` fires for each click in the viewport, and the state object includes `isDragging` and `isMoving` along with `position`, `maxPosition`, the viewport and content sizes, `dragOffset`, `dragAngle` and `borderCollision`. The important detail is that `event.defaultPrevented` is true for the click that ends a drag, so a gallery of links can be built with the rule that a click which follows a drag does not reach the link, and a plain click does. The pointer events `onPointerDown`, `onPointerMove`, `onPointerUp` and `onWheel` are also exposed, with the pointer release callback firing for a cancelled gesture as well as a completed one.

## Snapping is a function you write, not a setting

Paging behaviour is expressed as a `snap` function rather than as a set of options, and the shape of it is simple. The function receives the position where the content would come to rest and the state, and returns the position to scroll to, so a carousel that stops on a slide is one line of arithmetic. The documented recipe uses a slide width of 300 and rounds the resting position to the nearest multiple, then wires the previous and next buttons to `scrollBy` with a positive and negative offset, which the method documentation notes accumulates across repeated calls. The other movement methods follow the same pattern: `scrollTo` takes absolute coordinates and leaves a missing coordinate alone, `scrollBy` adds, `scrollIntoView` takes an element and an alignment of `nearest`, `start`, `center` or `end`, and `setPosition` jumps without animation. Positions never leave the edges.

## The package is ESM only and the size is enforced in the build

Installation is one command, `npm i scrollbooster`, or a script tag that exposes the global `ScrollBooster` from a CDN, and the simplest setup is a single constructor call with a viewport element. The package is ESM only, and the documentation is specific about what that means: `require()` loads it in Node 20.19 or later and 22.12 or later, and the type definitions for the options and the state are exported for TypeScript projects. The build is handled by tsdown, and the checks are thorough for a library this size. Linting is biome, testing is vitest with a chromium project for browser behaviour, type checking runs the main and test configurations, and the package check runs publint in strict mode, the are-the-types-wrong tool against the packed output with an ESM only profile, and size-limit, which is the step that keeps the library near the six kilobytes gzipped that the README claims. Releases go through changesets, and the tree carries a MIGRATION.md for the jump from the 3.x line, whose last release was v3.0.0 in 2020, to v4.0.0 published on 2026-10-09.

## Where it is the wrong tool

Three boundaries are worth stating. If your content relies on `position: sticky` or on scroll event listeners, the default transform mode will not give you those, and switching to `native` mode means giving up the elastic bounce, which is usually the reason to reach for a library like this at all. If your build pipeline still consumes CommonJS, the ESM only packaging is a blocker rather than an inconvenience, because the documentation offers no CommonJS build. And if the content is a normal document flow that should scroll the page, a library that intercepts wheel events and moves a transform is the wrong shape, since the wheel option already falls back to scrolling the page when the content cannot move further, which is a good default but also a reminder of the trade. The library's own domain is narrow and stated clearly: pleasant dragging of a specific container, with everything else left to the browser.

## Conclusion

scrollbooster suits a front end team that needs a gallery, a carousel or a wide panel to be draggable with a consistent feel across mouse, touch and pen, and it suits teams that care about the shipped size, since the package is about six kilobytes gzipped with no dependencies and a size limit check in the build. It is a poor fit if you need sticky positioning or scroll listeners inside the content, because the default transform mode moves the content rather than the scroller, and it is a poor fit for a CommonJS build pipeline, since the package is ESM only. Before you adopt it, check three things: which scroll mode your content needs, since the choice determines whether the browser's own scrolling behaviour survives, how the friction and bounce values behave on a high refresh rate display, since they are defined per 60 hertz frame, and the migration notes, because v4.0.0 published on 2026-10-09 is a major rewrite with a MIGRATION.md for people coming from the 3.x line.

## FAQ

### How do I install and use scrollbooster?

Install it with `npm i scrollbooster`, or load it from a CDN script tag as the global `ScrollBooster`. The simplest setup is `new ScrollBooster({ viewport: document.querySelector('.viewport') })`, and the viewport element is the only required option.

### What is the difference between the transform and native scroll modes?

The default transform mode moves the content with a CSS translate and allows it to bounce past the edges, but the viewport itself does not scroll, so sticky positioning and scroll listeners inside the content stop working. The native mode sets scrollLeft and scrollTop on the viewport instead, which keeps browser scrolling behaviour but gives up the bounce.

### Does scrollbooster work with CommonJS?

No, the package is ESM only. The documentation notes that require() loads it in Node 20.19 or later and 22.12 or later, and a script tag exposes the library as a global for browser use without a bundler.

### How do I make a carousel stop on each slide in scrollbooster?

Use the snap option, which receives the resting position and returns the position to scroll to. The documented recipe is `snap: (rest) => ({ x: Math.round(rest.x / slide) * slide })` with a slide width of 300, and the buttons then call `sb.scrollBy({ x: slide })` and `sb.scrollBy({ x: -slide })`.

### How does scrollbooster handle users who prefer reduced motion?

The reducedMotion option takes `auto`, `always` or `never`. With `auto`, which is the default, the library follows the prefers-reduced-motion setting, while `always` turns off inertia, bounce and smooth scrolling entirely.

## Sources

- [ilyashubin/scrollbooster on GitHub](https://github.com/ilyashubin/scrollbooster)
- [License: MIT](https://github.com/ilyashubin/scrollbooster/blob/master/LICENSE)
- [Project website](https://ilyashubin.github.io/scrollbooster)
- [README](https://github.com/ilyashubin/scrollbooster/blob/master/README.md)
- [Releases](https://github.com/ilyashubin/scrollbooster/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ilyashubin-scrollbooster
