Motion Background
Ambient, living backgrounds that sit behind content without stealing focus. The goal is subtle, seamlessly looping motion that stays readable and performant. This skill is self-contained: every technique below ships its own runnable code — CSS, GLSL, and canvas — with no external dependencies beyond Three.js where a shader is involved.
When to use
- Hero/landing background that subtly moves; section ambience.
- Login/splash/empty-state living backdrops.
- A gradient, mesh, aurora, shader, or particle/constellation field behind text or UI.
Pick an approach
Default to CSS if a gradient suffices — it is the cheapest and most reliable. Use a shader for organic flowing color; use canvas particles for a constellation/network look.
Design rules
- Subtle: low contrast versus content, slow motion (long periods, 8–30s loops), nothing that competes with text.
- Readable: keep contrast for foreground text; add a scrim (
linear-gradientoverlay orbackdrop) if needed. - Seamless loop: drive motion with periodic functions so time wraps with no visible jump (see below).
- Respect motion preferences and battery: honor
prefers-reduced-motion, and pause when offscreen or the tab is hidden.
Core techniques (inlined, runnable)
1. Animated CSS mesh gradient (no JS)
Layered radial gradients whose positions drift. Animating background-position on a larger-than-viewport gradient loops seamlessly.
The 0% and 100% keyframes are identical, so the loop has no seam.
2. Full-screen GLSL gradient + noise shader (Three.js)
A flowing aurora/gradient using value noise in a fragment shader on a full-screen plane. uTime advances each frame; to loop, feed it a wrapped time (section 4).
3. Canvas constellation particles
Drifting points connected by lines when near — the classic "network" background. Pure canvas 2D, no dependencies.
The O(n²) link loop is fine to ~120 points; above that, spatial-hash into a grid and only test neighboring cells.
4. Seamless loop technique
For canvas/JS, wrap time into a fixed period so motion repeats exactly. Use the loop phase (0→2π) as the argument to periodic functions:
Any motion built only from sin/cos of phase (or integer multiples) loops seamlessly. In the shader, feed uTime = phase and use only sin/cos of it; for noise-scrolled gradients, scroll by an integer number of noise cells per period so the field tiles.
5. Reduced motion + offscreen/hidden pause
When running is false, skip the render inside the rAF loop (as in section 2) — the loop stays alive to resume cheaply, but does no GPU/CPU work.
Deliver & verify (standalone HTML)
Packaged helper (
scripts/):scripts/seek-shot.sh anim.html 0 1.5 3freezes the?t=Nharness and screenshots each moment;scripts/contact-sheet.sh sheet.png frame-*.pngtiles them for one-glance review. Seescripts/README.md.
A motion background ships as one .html file that opens directly in a browser — markup, the background, and (for shader/canvas) Three.js or canvas JS from CDN, all inline. One file is the right tier.
Output contract:
- One
.htmlfile: the background layer + a sample of foreground text on top to check readability/contrast. - One animation driver — the CSS
@keyframes, the shader rAF loop, or the canvas rAF loop; just one. - Include the freeze harness below, matched to the technique, so any moment can be screenshotted deterministically.
Freeze harness — pin a frame for screenshots. Match the mechanism to the technique:
Verify loop — render → freeze → screenshot → check:
- Open the file frozen at start / mid / end across one loop period:
…/bg.html?t=0,?t=<period/2>,?t=<period>. - Screenshot each frozen frame.
- Check fidelity (subtle, on-brand, seamless) and artifacts — text contrast holds at every frame, no banding in gradients/shader, the loop seam (
t=0vst=period) matches, no GPU/console errors.
Before you finish:
- Opens standalone in a browser — no console/WebGL errors, no missing CDN.
- One driver;
?t=Nfreezes the exact frame (shader renders one frame, canvas sim stepped deterministically). - Screenshotted at start / mid / end — foreground text stays readable, no banding, loop seam invisible.
prefers-reduced-motionhonored (one static frame rendered, loop stopped) + pauses offscreen/hidden.devicePixelRatiocapped at 2; motion is slow and subtle, never out-contrasting content.
Quick reference
Gotchas
- Never let the background out-contrast the foreground text — add a scrim if it does.
- Uncapped
devicePixelRatioon retina/4K murders the GPU; cap at 2. background-positionloops only if first and last keyframes match exactly.- Noise-scrolled shaders loop only if the scroll is an integer number of cells per period; otherwise the loop visibly jumps.
- Forgetting the offscreen/hidden pause drains battery on mobile even when the user can't see it.
Reference files
references/background-recipes.md— fuller drop-in implementations: a richer multi-blob CSS mesh with blur, the complete Three.js aurora shader with mouse parallax and a true seamless-loop time wrap, a grid-optimized constellation field with mouse repulsion, a perfectly-looping noise-flow technique, and a complete reduced-motion + offscreen-pause manager wrapping all of them.

