LIGHT. MATH. YOUR NEXT BUILD.
Performance and accessibility
Start with a rendering budget
A full-screen shader costs more than a small decorative panel. Begin with a moderate pixel budget and frame-rate limit, then measure the finished page on target hardware.
background.update({ maxPixels: 700000, maxFPS: 30 });
These example limits trade detail and motion smoothness for lower rendering cost. They do not guarantee a particular frame rate. Large displays, multiple canvases, transparent layers, complex scenes, and device thermal limits all matter.
Respect reduced motion
const motion = matchMedia('(prefers-reduced-motion: reduce)');
const syncMotion = () => background.update({ paused: motion.matches });
syncMotion();
motion.addEventListener('change', syncMotion);
// During teardown:
// motion.removeEventListener('change', syncMotion);
// background.destroy();
Combine this preference with any explicit pause control you provide. Do not automatically resume an animation that the visitor chose to pause.
Pause work the visitor cannot see
Use an IntersectionObserver to pause an offscreen background, and respond to the document's visibility state. Keep configuration state separate from visibility state so returning to the page resumes only when appropriate.
let visible = true;
let userPaused = false;
const sync = () => background.update({
paused: userPaused || !visible || document.hidden || motion.matches,
});
const observer = new IntersectionObserver(([entry]) => {
visible = entry.isIntersecting;
sync();
});
observer.observe(document.querySelector('.hero'));
document.addEventListener('visibilitychange', sync);
// On teardown, disconnect observer and remove visibility/preference listeners.
Keep the content usable
- Keep the canvas decorative and separate from meaningful text.
- Use a readable palette and test contrast across animation states.
- Keep links and form controls usable by keyboard.
- Provide an accessible pause button for persistent animation.
- Use a CSS color or poster fallback when WebGL is unavailable.
- Call
destroy()when a component unmounts.
Use video posters for large galleries instead of running every shader simultaneously. A small number of visible live previews is a better starting point than dozens of WebGL contexts.
Freeze pointer interaction when pausing Prism
A time pause can leave cursor-driven deformation active. For a full freeze in Prism, set interaction to zero while paused and restore your configured value when resuming. Use one shared pause-state function so a visibility or preference change cannot accidentally override the visitor's Pause button.
// background is the mounted Prism instance; restore your own configured value.
const configuredInteraction = 1;
const preference = matchMedia('(prefers-reduced-motion: reduce)');
let userPaused = false;
function syncPause() {
const paused = userPaused || document.hidden || preference.matches;
background.update({
paused,
interaction: paused ? 0 : configuredInteraction,
});
}
// Call this from your own accessible Pause / Resume button.
function togglePause() {
userPaused = !userPaused;
syncPause();
}
preference.addEventListener('change', syncPause);
document.addEventListener('visibilitychange', syncPause);
syncPause();
// On teardown, remove these listeners and call background.destroy().
Use this shared function in place of the simpler preference-only listener above when your page also has a manual pause control. Keep offscreen visibility in the same state calculation if you add an IntersectionObserver.