Skip to content

GoBoldlyForward/hamsterwheel

Repository files navigation

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/hamsterwheel

Or 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 down

How it works

  1. Wrap. All of the host's current children move into one .hamsterwheel__unit div.
  2. Clone. The plugin appends clones deep copies of the unit (ids stripped, aria-hidden added).
  3. Watch. A passive window.scroll listener tracks direction.
  4. 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.
  5. Autoscroll. A requestAnimationFrame loop adds speed × dt pixels to scrollY each frame. wheel events with |deltaY| ≥ flipThreshold against 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 `
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 listeners

The 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.
  • scrollDeltaflipThreshold (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 window scroll. The plugin reads and writes window.scrollY on its page. For a contained loop, mount it inside an iframe.
  • Anchor links die at the seams. #section hash 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. Keep clones between 2 and 6 on real sites.
  • Form state and timers duplicate. An <input> shows up clones+1 times; inline setInterval runs 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.

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

0 stars

Watchers

10 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors