@goboldlyforward/hamsterwheel
v2.0.0
Published
Turn any standard website into an infinite scroll loop. Vanilla-JS plugin, no build step — the modern successor to the 2016 jQuery plugin.
Maintainers
Readme
hamsterwheel
Take a standard website and make it loop — so you can scroll forever.
The plugin clones the host element's content N times and watches window scroll. When you'd hit the bottom edge, it silently teleports you back one unit-height; same for the top. Every teleport lands on visually identical content, so the seam reads as nothing — just an endless scroll.
Demo
goboldlyforward.github.io/hamsterwheel — scroll the framed sample site, watch the loop counter tick.
Install
npm install @goboldlyforward/hamsterwheelOr grab the files directly:
<link rel="stylesheet" href="path/to/hamsterwheel.css">
<script src="path/to/hamsterwheel.js"></script>Usage
Drop a single attribute on the element you want to loop. The plugin auto-mounts on DOMContentLoaded.
<main data-hamsterwheel data-clones="4">
<!-- your normal page content -->
</main>Or instantiate it programmatically:
const wheel = new Hamsterwheel('main', {
clones: 6,
autoscroll: true,
speed: 120, // px per second
direction: 'down', // or 'up'
});
wheel.start(); // begin (or resume) autoscroll
wheel.pause(); // stop autoscroll (manual loop still works)
wheel.reverse(); // flip direction
wheel.setOptions({ speed: 200 });
wheel.destroy(); // tear it all downHow it works
- Wrap. All of the host's current children move into one
.hamsterwheel__unitdiv. - Clone. The plugin appends
clonesdeep copies of the unit (ids stripped,aria-hiddenadded). - Watch. A passive
window.scrolllistener tracks direction. - Teleport. When the viewport bottom touches the wheel bottom while scrolling down, jump
scrollY -= unitHeight. Mirror for upward. Visible content is identical before and after — so it reads as continuous scroll. - Autoscroll. A
requestAnimationFrameloop addsspeed × dtpixels toscrollYeach frame.wheelevents with|deltaY| ≥ flipThresholdagainst the direction will flip it (so a hard upward flick reverses an autoscroll-down).
Options
| Option | Default | Notes |
| --------------------- | --------- | ----- |
| clones | 4 | extra copies appended after the original |
| autoscroll | false | start scrolling on its own |
| speed | 60 | autoscroll speed in CSS pixels per second |
| direction | 'down' | 'down' or 'up' |
| flipOnReverseScroll | true | a hard wheel against the autoscroll flips it |
| flipThreshold | 40 | single wheel-event |deltaY| that triggers a flip |
| hideScrollbar | false | suppress the scrollbar (selling the illusion) |
| autoStart | true | call init() in the constructor |
The same options read as data-* attributes on the auto-mounted host: data-clones, data-speed, data-direction, data-autoscroll, data-hide-scrollbar, data-flip-threshold.
API
Hamsterwheel.initAll(); // scan for [data-hamsterwheel]; idempotent
new Hamsterwheel(target, options?); // mount one element (selector or node)
wheel.start(); // begin autoscroll
wheel.pause(); // stop autoscroll; manual loop still works
wheel.resume(); // alias for start
wheel.stop(); // alias for pause
wheel.reverse(); // flip autoscroll direction
wheel.setDirection('up' | 'down');
wheel.setOptions(patch); // live-tune anything in the options table
wheel.destroy(); // remove clones, unwrap, detach listenersThe plugin assigns itself to el.__hamsterwheel after auto-mount, so you can fetch it later: document.querySelector('[data-hamsterwheel]').__hamsterwheel.
From the 2016 jQuery plugin
The original hamsterwheel shipped in 2016 as $.fn.hamsterWheel from Polar Notion. The concept hasn't changed; the implementation has. If you're migrating:
$('main').hamsterWheel(opts)→new Hamsterwheel('main', opts)scrollSpeed(ms per pixel) →speed(pixels per second). Multiply:speed ≈ 1000 / scrollSpeed.scrollDelta→flipThreshold(applies per wheel event instead of cumulative scroll).scrollbar: false(v1 default) →hideScrollbar: false(v2 default; opt in if you want it).autoscroll: true(v1 default) →autoscroll: false(v2 default; opt in).- v1 cloned only the host's first child. v2 wraps all children together and clones that — usually what you wanted.
Known limitations
- It owns
windowscroll. The plugin reads and writeswindow.scrollYon its page. For a contained loop, mount it inside an iframe. - Anchor links die at the seams.
#sectionhash links scroll to the original, but the user may have been teleported into a clone since. - Heavy DOM blows up. Final DOM size is
(clones + 1) × unit. Keepclonesbetween 2 and 6 on real sites. - Form state and timers duplicate. An
<input>shows upclones+1times; inlinesetIntervalruns that many times too.
Requirements
HTML, CSS, and ~5KB of JavaScript. No framework, no build step. Uses requestAnimationFrame, ResizeObserver, and passive scroll listeners.
License
MIT — see LICENSE. Original 2016 copyright (Polar Notion) preserved alongside the 2026 rewrite.
