Pattern · 26
Infinite wrap
无限折返 · infinite-wrapRemap a finite strip to the copy nearest the viewport so dragging has no visible end.
An endless gallery does not need an endless DOM. Each card chooses the periodic copy nearest the viewport centre. Its jump between copies must happen outside the visible area; a period that is too short exposes the seam.
Formula
period = itemCount × spacing xᵢ = wrapCentered(i × spacing − offset, period) visible jump-free if period > viewport + item width
Parameters
| This page | Mature libraries | |
|---|---|---|
| period | n × spacing | —Distance between two copies of the same card. |
| placement | wrapCentered | —Always pick the periodic copy nearest the centre. |
| offset | accumulated drag | — |
Doing it well
- Choose the period first, then place each card.
wrapCenteredhandles negative offsets; raw%is insufficient on the negative side.www-motion/rewrite/lib/case-runtime.js:4–5 - Move the relocation point outside the viewport: the period must exceed the viewport plus one card width. The short-period mode makes the jump visible.www-motion/rewrite/lib/motion-kit.js:40–49
- You can recompute the nearest copy each frame instead of cloning edge nodes and teleporting the whole track.www-motion/rewrite/lib/motion-kit.js:40–49
In our cases
27 studies use infinite wrapping. dimension uses edge copies for its chain; the rewrite library also provides wrapNearest, which chooses the nearest copy per tile each frame. Both approaches hide the same seam.
Used in 27 cases
Reference implementation · basics.js
/**
* Position of item i on an endless strip scrolled by `offset`. Each item is drawn at the copy
* nearest the centre, so it jumps by one full period — make the period wider than the viewport
* plus one item, and the jump always happens off-screen.
*/
export const wrapPlacement = (i, n, offset, spacing) => wrapCentered(i * spacing - offset, n * spacing);
/** x folded into [0, period) — also for negative x, unlike `%`. */
export const wrap = (x, period) => ((x % period) + period) % period;
/** x folded into [-period/2, period/2): "how far from the centre copy". */
export const wrapCentered = (x, period) => wrap(x + period / 2, period) - period / 2;