Digital experiences · · 6 min read
Three.js: pause offscreen animation and respect reduced motion
Add a small rendering controller to a decorative 3D scene so it runs only when useful, supports a pause control, and preserves a still view for reduced-motion visitors.
By Sociologix Editorial

Make the 3D scene a considerate part of the website
A rotating product or sculptural hero can help explain a digital service. It should not keep scheduling decorative animation after a visitor scrolls down to a quote form or switches tabs. For an AI-enabled website, the content, navigation and customer-care controls need to remain useful whether the 3D scene is moving or still.
This AI-assisted editorial tutorial uses primary documentation checked October 7, 2026. The controller below passed synthetic scheduling and cleanup tests in Node.js 24.19.0. It has not been benchmarked on a physical device or exercised against a real WebGL renderer for this article. Treat it as an integration example, not a measured performance claim.
1. Decide when a frame is useful
Three.js documents rendering on demand for scenes that change only after interaction, resizing or asset loading. A decorative animation needs repeated frames while it is moving, but a still scene can be redrawn only when something changes. This example combines those two modes. [1]
Use two separate signals: IntersectionObserver reports whether the canvas intersects the viewport, while Page Visibility reports whether the document is hidden. The ordinary intersection check is geometric; it does not establish that another element is not covering the canvas. Pause explicitly when a full-screen modal obscures your artwork. [2][3]
Also read prefers-reduced-motion. It represents a visitor preference to reduce nonessential motion. Here that preference stops automatic rotation while allowing a single still frame when needed. Keep a separate, keyboard-accessible pause button for visitors who want to stop motion manually. [4]
2. Give one controller ownership of the loop
Save the following function as frame-gate.js in an existing browser-based Three.js project. It expects a canvas element, a draw callback and an advance callback. This is for a plain Three.js scene; do not add a competing loop inside a React Three Fiber application that already owns rendering.
Remove the scene’s previous animation loop before attaching this controller. If that loop uses renderer.setAnimationLoop, the documented way to stop it is setAnimationLoop(null). Independently scheduled requestAnimationFrame callbacks need their own cancellation. Two owners can otherwise keep drawing the same canvas. [5]
The controller keeps at most one animation callback queued. It resets elapsed time when visibility or motion settings change, and caps an individual animation step at 0.05 seconds. That cap is our design choice for decorative motion; it intentionally drops excess elapsed time and is not a physics clock.
export function createFrameGate(element, draw, advance) {
const motion = matchMedia('(prefers-reduced-motion: reduce)');
let visible = false, paused = false, disposed = false;
let frameId = null, previous = null;
const canDraw = () => !disposed && visible && !document.hidden;
const canAnimate = () => canDraw() && !paused && !motion.matches;
function cancel() {
if (frameId !== null) cancelAnimationFrame(frameId);
frameId = null;
previous = null;
}
function request() {
if (canDraw() && frameId === null) frameId = requestAnimationFrame(tick);
}
function tick(time) {
frameId = null;
if (!canDraw()) { previous = null; return; }
if (canAnimate()) {
const seconds = previous === null ? 0 : Math.min((time - previous) / 1000, 0.05);
previous = time;
advance(seconds);
} else previous = null;
draw();
if (canAnimate()) request();
}
function sync() { cancel(); request(); }
const observer = new IntersectionObserver(entries => {
if (disposed) return;
for (const entry of entries) {
if (entry.target === element) visible = entry.isIntersecting;
}
sync();
});
observer.observe(element);
document.addEventListener('visibilitychange', sync);
motion.addEventListener('change', sync);
return {
invalidate: request,
setPaused(value) { paused = Boolean(value); sync(); },
dispose() {
disposed = true;
cancel();
observer.disconnect();
document.removeEventListener('visibilitychange', sync);
motion.removeEventListener('change', sync);
},
};
}3. Connect drawing, resizing and asset loading
The integration below assumes an existing renderer, scene, perspective camera and object named sculpture. Give the canvas an explicit CSS display size. The advance callback changes the object; the draw callback renders the current scene. Using elapsed time instead of a fixed rotation per frame avoids tying speed to the display refresh rate. [6]
Call invalidate after changing the camera, replacing a material or finishing an asset load. In reduced-motion or manually paused mode, that requests a still update without restarting automatic rotation. Resize handling must also update the camera projection before the next draw. [1][5]
Connect a native button to setPaused and expose its state with aria-pressed. Do not hide the contact form or product details in the canvas. On teardown, remove your resize and button listeners, dispose the controller, and release the Three.js resources owned by the scene. The controller only cleans up its own scheduling and listeners.
import { createFrameGate } from './frame-gate.js';
// renderer, scene, camera and sculpture already exist.
const gate = createFrameGate(
renderer.domElement,
() => renderer.render(scene, camera),
seconds => { sculpture.rotation.y += seconds * 0.25; }
);
function resize() {
const canvas = renderer.domElement;
const width = canvas.clientWidth, height = canvas.clientHeight;
if (!width || !height) return;
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
gate.invalidate();
}
window.addEventListener('resize', resize);
resize();
// After a texture/model finishes loading: gate.invalidate();
// Your pause button calls gate.setPaused(true or false).
// On teardown: remove the resize listener, then gate.dispose().4. Test the transitions in both directions
Open your implementation with a visible canvas and check that motion starts. Scroll it fully offscreen and return. Switch away from the tab and come back. The first resumed frame should not jump forward to compensate for all the time the visitor spent elsewhere.
Toggle the operating system’s reduced-motion preference while the page is open. The current object pose should become still, then resume if the preference is removed and the manual pause is off. A new texture should still appear after invalidate. Test the pause button using keyboard focus and touch.
Our synthetic checks covered initial offscreen state, repeated invalidation, elapsed-time capping, scrolling out and back, hidden-tab transitions, reduced motion, manual pause, listener cleanup and late callbacks after disposal. They checked scheduling decisions, not browser rendering quality or battery use.
- Confirm that disposing the scene leaves no scheduled frame or observer listener.
- Check that a modal pauses the artwork even when the canvas remains in the viewport.
- Test asset loading and resizing while motion is disabled.
- Keep useful HTML content and a fallback visual available when WebGL cannot run.
5. Measure the whole customer journey
Record a browser performance trace on representative mobile hardware with the scene visible, offscreen and paused. Inspect frame work alongside input responsiveness while opening your AI assistant or completing a quote form. A scheduling change alone does not prove that a heavy model or expensive shader is acceptable.
This controller does not reduce geometry, texture downloads, shader complexity or memory retention. Profile those separately. The practical goal is an experience whose visual work supports the visitor’s next action, with a still alternative that remains clear and usable.
Sources & further reading
Build a 3D website that works beyond the first impression
Sociologix can help connect purposeful 3D design, accessible interaction, AI assistance and lead intake. Bring one page and the action you want visitors to complete.
Talk to Sociologix