Programmable scroll physics engine for the web. Per-section friction, magnetic snap points, configurable mass and inertia.
~4KB | TypeScript | Zero dependencies | Pure computation
npm install @aumiqx/scrollimport{ScrollEngine}from"@aumiqx/scroll";constengine=newScrollEngine({mass: 1.2,friction: 0.95,min: 0,max: 3000,zones: [{start: 0,end: 600,friction: 0.85},// slow hero{start: 600,end: 1200,friction: 0.97},// fast gallery{start: 1200,end: 1800,snap: true},// snap pricing],magnets: [{position: 1500,strength: 0.4,range: 120},],});// On wheel events:element.addEventListener("wheel",(e)=>{e.preventDefault();engine.applyForce(e.deltaY*0.3);},{passive: false});// On every frame:functiontick(){conststate=engine.tick();element.style.transform=`translateY(${-state.position}px)`;requestAnimationFrame(tick);}tick();Every website on Earth uses identical scroll physics. Same mass, same friction, same inertia. The browser's scroll engine is a black box you can't modify.
This library replaces it with a physics engine where every parameter is yours.
| Feature | Native Scroll | Lenis | GSAP ScrollTrigger | @aumiqx/scroll |
|---|---|---|---|---|
| Custom friction | No | Partial | No | Per-section |
| Magnetic snap | CSS only | No | No | Configurable |
| Custom mass | No | No | No | Yes |
| Bounce walls | Platform-specific | No | No | Elastic |
| Pure computation | N/A | DOM-dependent | DOM-dependent | No DOM |
Create a scroll physics engine.
constengine=newScrollEngine({mass: 1.2,// scroll inertia (higher = more momentum)friction: 0.95,// velocity decay per frame (0.80-0.99)min: 0,// scroll bounds minimummax: 3000,// scroll bounds maximumzones: [],// per-section friction overridesmagnets: [],// magnetic snap pointswalls: [],// custom bounce points});Apply external force from wheel/touch events. Divided by mass internally.
element.addEventListener("wheel",(e)=>{engine.applyForce(e.deltaY*0.3);});Advance physics by one frame. Call on every requestAnimationFrame.
conststate=engine.tick();// state.position - current scroll position (px)// state.velocity - current speed (px/frame)// state.activeZone - which zone index is active (-1 if none)// state.nearestMagnet - magnet index in range (null if none)// state.isBouncing - whether hitting a wallJump to a position. Resets velocity.
engine.setPosition(1200);// teleport to pricing sectionUpdate physics at runtime.
// User enables "reduced motion"engine.configure({mass: 0.5,friction: 0.85});Read current state.
interfaceScrollZone{start: number;// zone start (px)end: number;// zone end (px)friction?: number;// override friction (0.80-0.99)snap?: boolean;// pull to center when slow}Examples:
zones: [{start: 0,end: 600,friction: 0.82},// hero: heavy, cinematic{start: 600,end: 1200,friction: 0.975},// gallery: light, momentum{start: 1200,end: 1800,snap: true},// pricing: magnetic center{start: 1800,end: 2400,friction: 0.80},// cta: maximum resistance]interfaceScrollMagnet{position: number;// target position (px)strength: number;// pull force (0-1)range: number;// activation radius (px)}Example:
magnets: [{position: 1500,strength: 0.4,range: 120},// pricing{position: 2100,strength: 0.5,range: 100},// cta]- Storytelling landing pages - hero is slow, gallery is fast, CTA snaps
- WebGL camera control - mass and inertia create cinematic camera movement
- Reading experiences - long articles automatically slow scroll down
- E-commerce - product listings glide, checkout anchors
- Data visualization - scroll drives chart progression with zone-based pacing
- Scroll-driven games - scroll IS the game input with custom physics
Each frame:
- Force - wheel/touch input divided by mass
- Friction - velocity multiplied by zone-specific friction (0.80-0.99)
- Magnets - nearby magnets apply pull proportional to proximity
- Position - updated by velocity, clamped to bounds with bounce
velocity += force / mass
velocity *= zoneFriction
velocity += magnetPull
position += velocity
MIT