DEV Community

Axit
Axit

Posted on • Originally published at aumiqx.com

Every Website on Earth Uses the Same Scroll Physics. Why?

Open any website. Scroll. Now open a different website. Scroll again.

Same feel. Same momentum. Same friction. Same inertia.

Every website on Earth uses identical scroll physics because the browser's scroll engine is a black box. You can listen to events, smooth them (Lenis), animate things on scroll (GSAP ScrollTrigger). But you cannot change how scroll feels.

Why should a long-form article scroll at the same speed as a photo gallery? Why should a CTA section fly past at the same momentum as your hero?

Introducing @aumiqx/scroll

A programmable scroll physics engine for the web. ~4KB. Zero dependencies. Pure computation — it calculates where scroll should be, you decide what moves.

import { ScrollEngine } from "@aumiqx/scroll"

const engine = new ScrollEngine({
  mass: 1.2,          // heavier = more momentum
  friction: 0.95,     // lower = stops faster
  zones: [
    { start: 0, end: 600, friction: 0.82 },     // hero: heavy, cinematic
    { start: 600, end: 1200, friction: 0.975 },  // gallery: featherlight
    { start: 1200, end: 1800, snap: true },       // pricing: magnetic snap
  ],
  magnets: [
    { position: 1500, strength: 0.4, range: 120 },
  ],
})
Enter fullscreen mode Exit fullscreen mode

Now your hero section resists. Your gallery glides. Your pricing section magnetically grabs the user and holds them there.

The Physics

Every frame, four things happen:

velocity += force / mass        // input from wheel/touch
velocity *= zoneFriction        // per-section decay
velocity += magnetPull           // nearby magnets attract
position += velocity             // update scroll position
Enter fullscreen mode Exit fullscreen mode

That's the entire engine. Four lines of math, 60 times per second.

The engine doesn't touch the DOM. It computes position. You apply it however you want — CSS transforms, Canvas, WebGL camera, or just window.scrollTo():

element.addEventListener("wheel", (e) => {
  e.preventDefault()
  engine.applyForce(e.deltaY * 0.3)
}, { passive: false })

function tick() {
  const state = engine.tick()
  element.style.transform = `translateY(${-state.position}px)`
  requestAnimationFrame(tick)
}
tick()
Enter fullscreen mode Exit fullscreen mode

Zone Types

The power is in zones — regions of your page where scroll physics change:

Zone Type Friction What It Feels Like
Reading 0.82 Heavy. Every pixel matters. Content demands attention.
Gallery 0.975 Featherlight. Momentum carries you through. Browsing mode.
Snap magnetic Pulls to center when you slow down. Can't accidentally skip.
CTA 0.80 Maximum resistance. The call-to-action anchors you.

Each zone is defined by start/end positions and a friction override:

zones: [
  { start: 0, end: 600, friction: 0.82 },       // hero
  { start: 600, end: 1200, friction: 0.975 },    // features
  { start: 1200, end: 1800, snap: true },         // pricing (snaps to center)
  { start: 1800, end: 2400, friction: 0.80 },    // CTA
]
Enter fullscreen mode Exit fullscreen mode

Magnetic Snap Points

Important content gets magnets — invisible attraction points that pull scroll toward them when you're nearby:

magnets: [
  { position: 1500, strength: 0.4, range: 120 },  // pricing
  { position: 2100, strength: 0.5, range: 100 },   // CTA
]
Enter fullscreen mode Exit fullscreen mode

The pull is proportional to proximity — stronger as you get closer, zero outside the range. Users can't accidentally fly past your pricing table.

Configurable Mass

One number changes the entire feel of your site:

  • mass: 0.5 — Snappy, responsive. Stops quickly. Good for utility apps.
  • mass: 1.2 — Balanced. Natural momentum.
  • mass: 4.0 — Heavy, cinematic. A single flick carries you through sections. Feels like pushing something physical.

Mass divides the input force: velocity += force / mass. Heavier scroll responds less to each input, but carries more momentum once moving.

How It Compares

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
Size Built-in 5KB 25KB+ ~4KB

What You Can Build

Storytelling Landing Pages — Hero = slow and dramatic. Gallery = fast and fluid. CTA = magnetic snap. Each section of your page has different scroll physics, creating a journey, not just a page.

WebGL Camera Control — Use scroll physics to drive a 3D camera. Mass and inertia create Steadicam-like movement instead of mechanical stepping. Combine with zones to slow the camera for reveals and speed up for transitions.

Reading Experiences — Long-form articles automatically increase friction. Readers absorb more content without consciously stopping to read. Combine with magnets at key paragraphs to create natural "reading rest points."

E-commerce Product Scroller — Product listings glide with low friction for fast browsing. Category changes snap into place. The checkout CTA has maximum friction and a magnet — you can't scroll past it by accident.

Scroll-Driven Games — A game framework where scroll IS the primary input. Platformers, runners, and puzzle games controlled entirely by scroll physics. The engine becomes the game engine.

Runtime Reconfiguration

Change physics on the fly — respond to user preferences or viewport changes:

// User enables "reduced motion"
engine.configure({ mass: 0.5, friction: 0.85 })

// Viewport resized — recalculate zones
engine.configure({ max: document.body.scrollHeight })
Enter fullscreen mode Exit fullscreen mode

Full State Introspection

Every frame, the engine returns complete state:

const state = engine.tick()

state.position      // current scroll position (px)
state.velocity      // current speed (px/frame)
state.activeZone    // which zone index (-1 if none)
state.nearestMagnet // magnet index in range (null if none)
state.isBouncing    // hitting a wall
Enter fullscreen mode Exit fullscreen mode

Use velocity for parallax effects, active zone for section highlighting, magnet proximity for visual feedback.

Try It Live

We built an interactive demo with 5 zones you can scroll through. Each zone has different physics — you'll feel the difference immediately. There's a mass slider so you can experience heavy vs light scroll in real-time.

Install

npm install @aumiqx/scroll
# or
pnpm add @aumiqx/scroll
Enter fullscreen mode Exit fullscreen mode

GitHub | npm | Live Demo

MIT licensed. TypeScript. Zero dependencies. ~4KB.


Built by Aumiqx — we build AI agents, workflow automations, and open-source tools that shouldn't work but do.

Top comments (0)