A 3D hero that does not break on mobile
September 20, 2026 · 4 min read
For Papilly I wanted a hero that really moves: a Three.js scene where the camera approaches the bed as you scroll. A 3D scene that costs the site its smoothness is not worth the effect, so here are the rules I kept.
Always ship a fallback image
The scene loads after everything else. So I render a real image on the server under the canvas and only remove it once the scene reports that its texture is ready. The result: never an empty card, and a proper image if WebGL is unavailable.
Only draw what is visible
A canvas running off screen heats up a phone for nothing. An IntersectionObserver pauses rendering when the scene leaves the screen, and I cap the pixel ratio so it never exceeds what the display can show.
Respect reduced motion
The motion preference is read with useSyncExternalStore on matchMedia. When it is on, the scene stays still. With React 19's rules, a setState inside an effect is rejected by the linter: this API is the right way to read a media query.
Load Three.js on demand
In Next, dynamic with ssr: false is only allowed from a Client Component. Each scene is therefore imported from a small client file, and Three.js stays out of the page's initial JavaScript.
The product never covers the headline
A sales headline has to stay readable. I anchor the 3D object to a spot in the DOM rather than hard-coded values, and the headline appears through CSS so it shows even if the script fails.
Next article
Unique Shopify themes without touching Liquid