No frames! Pure CSS
The pattern does not animate by drawing frames in JavaScript. There is no requestAnimationFrame loop, no <canvas> redraw, and no React or Vue render on every tick.
Geometry is computed once per size and options (src/core). The DOM renderer (src/js) injects a stylesheet and gives each lane a CSS animation. The browser composites those transforms.
What moves
Each lane is a .iim-track. Vertical tracks use iim-scroll. Horizontal tracks use iim-scroll-x. Both are translate3d from 0 to var(--iim-cycle), linear, infinite.
@keyframes iim-scroll {
from { transform: translate3d(0, 0, 0); }
to { transform: translate3d(0, var(--iim-cycle), 0); }
}
@keyframes iim-scroll-x {
from { transform: translate3d(0, 0, 0); }
to { transform: translate3d(var(--iim-cycle), 0, 0); }
}The stylesheet is injected once as #images-in-motion-style from src/js/style.ts.
Layout then sets per-track CSS, not new keyframes:
| Property | Source |
|---|---|
--iim-cycle | Negative cycle length in pixels |
animation-duration | cycle / speed in milliseconds |
animation-delay | Phase offset so neighbours do not line up |
animation-direction | reverse on odd lanes, normal on even |
Odd lanes travel the other way. Speeds stay constant inside a lane and differ between lanes. The sheet rotation (angle) is a CSS transform on .iim-sheet, not a per-frame spin.
Each track contains two copies of the image cycle. The last gap is part of the period, so the loop does not jump when it wraps.
Pause eases, then holds
Playback never tears down the tracks. Pause, resume, and hover stop/start ramp playbackRate on the CSS animations (about half a second), then animation-play-state holds the pose. prefers-reduced-motion and a hidden tab still snap.
shouldRunAnimation in src/core/playback.ts turns it off when:
pausedis trueprefers-reduced-motion: reduce- the document is hidden
- hover mode is
stopand the pointer is over the host - hover mode is
animateand the pointer is not over the host
See Install and usage.
What React and Vue do
Bindings create a host, call mountImagesInMotion on an inner stage, and call update / destroy. They do not own keyframes, layout math, or playback.
The decorative layer has pointer-events: none. Children (React children, Vue slot) sit above it and stay interactive.
What Expo and NativeScript do
There is no React Native or NativeScript mosaic. Expo and NativeScript host this same CSS renderer in a native WebView.
The same translate3d @keyframes (iim-scroll / iim-scroll-x) run inside that WebView. WKWebView and Android WebView composite those transforms. Do not add react-native-web, 'use dom', Reanimated, or Skia for the lanes.
Mount surfaces: Frameworks.
What this is not
| Not this | This instead |
|---|---|
requestAnimationFrame + style writes | CSS @keyframes |
| Canvas or WebGL | DOM tiles and CSS transforms |
| Per-frame React state | One mount, CSS runs |
| A React Native or NativeScript view | The same CSS renderer in a WebView |
| A video or GIF | Still images, looping translate |
JS still runs for mount, resize, image aspect measurement, SVG normalization, and playback flags. It does not run the scroll.
Related
- Frameworks to pick a mount
- Expo and NativeScript for the native WebView hosts
- API for layout options
- Bundle size for the JS payload
- Examples for live configs