Every site scrolls the same. Yours doesn't have to.
A small physics engine for scroll. Give each section its own friction, add magnets that pull readers toward what matters, and move anything with the position it returns. Pure math, no DOM, zero dependencies.
- size
- ~4KB
- size
- dependencies
- 0
- dependencies
- friction zones
- per-section
- friction zones
01 · The problem
Same page. Different physics.
Browser scroll
friction 0.95 everywhere
scroll here
@aumiqx/scroll
per-section friction + a magnet
scroll here
same force, same moment. watch where each one lands.
02 · Physics bench
Fling it. Then change the laws.
Velocity, last 4 seconds
0.0 px/f
Presets
Last peak
0.0
Settled in
0f
03 · How it works
Four steps. Every frame.
- F01
Force
A wheel or touch event pushes the scroll. Heavier mass, smaller push.
- f02
Friction
Each zone decays velocity at its own rate, every frame.
- M03
Magnets
Magnets in range pull the position toward their target.
- P04
Position
You get one number back. Move whatever you like with it.
velocity += force / mass·velocity *= zoneFriction·position += velocity
04 · Playground
Five sections. Five kinds of scroll.
- 1Scroll slowly and feel the hero resist
- 2Keep going, features let you glide
- 3Approach pricing, the magnet grabs you
- 4Set mass to 4 and flick hard
Scroll feels heavy here. First impressions deserve a slower pace, so the reader sinks in instead of flying past.
friction 0.82 · heavy
Now it feels light. One flick and momentum carries you, like flipping through cards in a gallery.
friction 0.975 · glide
This section has a magnet. Try to scroll past it and the engine pulls you back to the centre.
snap + magnet · hard to skip
A comfortable reading pace, tuned for testimonials and case studies.
friction 0.93 · reading pace
Maximum resistance plus a magnet. The call to action holds you in place.
friction 0.80 + magnet · anchored
scroll inside to feel each section
Mass
Higher mass means more momentum and a slower, more cinematic response.
Active zone
friction 0.82 · heavy
size
~4KB
deps
0
DOM
not needed
per tick
pure math
05 · The code
About twenty lines. Then it's yours.
import { ScrollEngine } from "./scroll" // from github.com/aumiqx/scroll
const engine = new ScrollEngine({
mass: 1.2,
friction: 0.93,
zones: [
{ start: 0, end: 500, friction: 0.85 },
{ start: 500, end: 1000, friction: 0.97 },
{ start: 1000, end: 1500, snap: true },
],
magnets: [{ position: 750, strength: 0.4, range: 120 }],
})
element.addEventListener("wheel", (e) => {
engine.applyForce(e.deltaY * 0.3)
})
function tick() {
const { position } = engine.tick()
el.style.transform = `translateY(${-position}px)`
requestAnimationFrame(tick)
}
tick()applyForce(delta)Feed input from any event.
tick()Advance the physics by one frame.
configure(opts)Change settings at runtime.
Install
Not on npm yet. Clone github.com/aumiqx/scroll and import the engine directly. It has no dependencies.
06 · What you could build
It outputs a number. Anything can follow it.
Storytelling landing pages
A slow, dramatic hero. A fast feature gallery. A CTA that holds you.
Cinematic WebGL
Drive a 3D camera with scroll. Mass gives you Steadicam movement.
Long-form reading
Raise friction in dense sections so readers slow down without noticing.
Product listings
Physical momentum through a catalogue, with a magnet on the checkout button.
Scroll-driven data stories
Zones map to datasets. Magnets land on the moments that matter.
Scroll games
Runners and puzzles controlled entirely by scroll. The engine becomes the game loop.
07 · Reference
Every option. At a glance.
Configuration
| Option | Type | Default | What it does |
|---|---|---|---|
| mass | number | 1 | Scroll inertia. Higher means more momentum and a slower response. |
| friction | number | 0.95 | Base velocity decay per frame. 0.80 stops fast, 0.99 glides. |
| min / max | number | 0 / Infinity | Scroll bounds. Hitting a bound bounces. |
| zones | ScrollZone[] | [] | Per-section friction overrides and snap behaviour. |
| magnets | ScrollMagnet[] | [] | Points that pull the scroll toward them when in range. |
| walls | ScrollWall[] | [] | Custom bounce points with configurable elasticity. |
API
applyForce(delta)- Apply external force from wheel or touch. Divided by mass.
tick()- Advance one frame. Returns position, velocity, zone, magnet and bounce state.
setPosition(pos)- Jump to a position and reset velocity.
configure(opts)- Update mass, friction, zones and magnets at runtime.
.position- Current scroll position.
.velocity- Current velocity in px per frame.
.state- Full state snapshot.
ScrollZone
{
start: number,
end: number,
friction?: number, // 0.80 to 0.99
snap?: boolean // pull to centre
}ScrollMagnet
{
position: number, // target px
strength: number, // 0 to 1
range: number // activation radius
}