9/18 | Validate feature ideas earlier with AI-driven prototypes

What are best AI tools? Take the State of AI survey

Builder.io
Builder.io
Contact sales
‹ Back to blog

AI

Recreating Apple-style 3D scroll animations in Three.js and WebGL

October 2, 2025

Written By Steve Sewell

How does Apple make those 3D objects glide and rotate as you scroll?

They actually use a pretty awful trick where they load 100s of images and flip through them as you scroll, which explains why the scrolling is so jittery. Yuck.

Let’s build something similar that listens to scroll but manipulates actual 3D objects super smoothly as you go.

It’s actually a lot easier than you’d think.

I will show how I rebuilt this quickly with AI, how the code works, and how you can create the same effect by hand.

Starting with a little boilerplate

3D scenes require many precise values. Camera distance, model scale, rotation easing. Typing that by hand is tedious. This is just one small example:

<Canvas camera={{ position: [0, 0, 3.2], fov: 35 }} style={{ height: "100vh" }}>
  <color attach="background" args={["#000"]} />
  <ambientLight intensity={0.4} />
  <Suspense fallback={null}>
    <Environment preset="city" />
    <ScrollRig progress={progress} />
  </Suspense>
</Canvas>

This is something LLMs are great at helping us with, so we don’t have to blindly guess and check so slowly. But, words alone can be ambiguous for motion, so how can we get a pretty good starting point from AI for nuanced scroll-based motion?

The trick I use is I give the AI a simple storyboard image with key states. Even just a hand drawn images like I used removes guesswork and lets the agent generate the scaffolding quickly.

Screenshot 2025-09-26 at 9.50.32 AM.png

I did this using Builder.io, where you can connect any repository of yours or start from scratch, open the visual editor, and visually prompt away.

I grabbed a free iMac GLTF from Sketchfab, attached it, and let the AI install dependencies and set up the scene.

The prompt I used was:

After the hero, I want to add a scroll animation with a 3D model of an iMac. 
I attached some storyboard illustrations.

During the scroll, the model should be viewed from the front, then rotate and translate to the left side, then rotate and translate to the top. 
This will give a front, side, and top view of the model.

[attached drawing of what I want]
[attached iMac GLTF file]

The first run got me pretty close but was zoomed in too far. I gave some feedback in a couple prompts to get sizing and spacing right, and after some iterations, things were looking solid. Remember to always give feedback to any AI agent, Builder.io or otherwise, to get the best results.

And if words aren’t cutting it, you can use design mode in Builder.io to have a full Figma-grade editor to get the details correct.

And that’s the basics. Using this technique of loading a 3D modal and transforming it on scroll unlocks a huge variety of cool things you can do.

Everything from moving objects, to panning through entire 3d worlds.

Now, let’s break down the code powering this so we can understand how it works.

The basic process is:

  1. Load a GLTF model and preload it.
  2. Track scroll progress between 0 and 1.
  3. Pass progress into a rig that updates rotation and position.
  4. Render with a black background, soft ambient light, and an HDRI environment for reflections.

We host the model on a CDN (I used builder.io/upload for this), then use DREI’s useGLTF to load it as a Three.js object. Preloading avoids a pop-in on the first frame.

import { useGLTF } from "@react-three/drei";

const MODEL_URL =
  "https://cdn.builder.io/o/assets%2FYJIGb4i01jvw0SRdL5Bt%2F2e3305fb7d814ae186cd10591e13c5c5?alt=media&token=cb550ba5-7f37-42b5-b887-ef62f1047183&apiKey=YJIGb4i01jvw0SRdL5Bt";

function ImacModel() {
  const { scene } = useGLTF(MODEL_URL);
  return <primitive object={scene} scale={1} />;
}
useGLTF.preload(MODEL_URL);

Tips

  • If the model is too large, lower model scale
  • Keep textures compressed and meshes decimated for performance.

This hook normalizes scroll to a 0 to 1 range and updates on scroll and resize. This lets us rig up our animation based on scroll % later.

import { useEffect, useState } from "react";

function useScrollProgress() {
  const [progress, setProgress] = useState(0);
  useEffect(() => {
    const handler = () => {
      const total = document.body.scrollHeight - window.innerHeight;
      setProgress(total > 0 ? Math.min(Math.max(window.scrollY / total, 0), 1) : 0);
    };
    handler();
    window.addEventListener("scroll", handler, { passive: true });
    window.addEventListener("resize", handler);
    return () => {
      window.removeEventListener("scroll", handler);
      window.removeEventListener("resize", handler);
    };
  }, []);
  return progress;
}

If your effect lives in a nested scroller, swap window.scrollY for the container’s scrollTop and scrollHeight.

The rig is a group that contains the model. On each frame we ease the rotation toward a target derived from progress. You can add position and scale for depth.

import { useRef } from "react";
import { useFrame } from "@react-three/fiber";

function ScrollRig({ progress }: { progress: number }) {
  const ref = useRef<THREE.Group>(null);
  useFrame((_, dt) => {
    if (!ref.current) return;

    const targetY = Math.PI * 0.5 * progress; // front to side over the scroll
    const lerp = Math.min(dt * 6, 1);

    ref.current.rotation.y += (targetY - ref.current.rotation.y) * lerp;

    // Add any additional transformations you like here - such as tilt, stop points, etc
  });
  return (
    <group ref={ref}>
      <ImacModel />
    </group>
  );
}

More things you can explore

  • Move the camera for parallax, keep the object anchored.
  • Animate material parameters like roughness or emissive for mood.
  • Define waypoints at 0, 0.33, 0.66, 1 and interpolate between them.

We are going to mount a full screen WebGL scene, set a camera, fill the background, add a soft light, load an environment map, and render the rig that reacts to scroll.

import { Canvas } from "@react-three/fiber";
import { Suspense } from "react";
import { Environment } from "@react-three/drei";

export default function ScrollImac() {
  const progress = useScrollProgress();

  return (
    // Outer scroller gives room to scrub
    <div style={{ height: "200vh", background: "#000" }}>
      {/* 
        Canvas is the React Three Fiber root. It creates the WebGL renderer and a camera 
        Camera position is [x, y, z]. Pull back on z so the model fits in frame
        fov is field of view in degrees. Lower is telephoto, higher is wide
      */}
      <Canvas
        camera={{ position: [0, 0, 3.2], fov: 35 }}
        style={{ height: "100vh" }} // Pin the scene to the viewport
      >
        {/* Scene background color */}
        <color attach="background" args={["#000"]} />

        {/* Soft base light that affects all surfaces equally. No shadows */}
        <ambientLight intensity={0.4} />

        {/* Wait for async assets like the environment map and GLTF model */}
        <Suspense fallback={null}>
          {/* Image based lighting and reflections. 'city' is a bright HDRI preset */}
          <Environment preset="city" />

          {/* Animated group that maps scroll progress to transforms */}
          <ScrollRig progress={progress} />
        </Suspense>
      </Canvas>
    </div>
  );
}

Tuning tips

  • Lower ambientLight intensity or pick a darker environment if the scene looks washed out
  • Reduce material envMapIntensity or choose a softer HDRI if reflections are too strong
  • Reduce model scale, move the camera back on z, or slightly increase fov if the object clips the frame
  • Add a very low intensity directionalLight from above and a bit to the side if the scene feels flat
  • Keep fov modest for a clean product look and avoid wide angle distortion
  • Test on slower devices and adjust lerp speed in useFrame if motion feels jumpy

Personally, I hate writing boilerplate, so I let tools handle it. I prefer when the tool is visual so I can see what I am working on.

If you have ever wondered how AAA games create those immersive environments, you have probably used engines like Unreal Engine 4 and noticed you do not hand code every detail like you would in CSS. You get a full suite of visual tools so game designers can build interactions and scenes visually, and it all still connects to real code.

send pr.mp4

The web has been behind on that kind of tooling. That gap is a big reason a lot of web app UIs look boring. The good news is this is changing.

Platforms similar to Unreal, including Builder.io, give you a visual surface that works directly with your codebase. You can make precise edits, iterate quickly, and see results as you go. When you are ready, click Send PR to open a pull request.

Designers get a powerful canvas that goes far beyond classic design files. Developers get clean, well formatted pull requests that slot into the normal review flow.

If you want something moved or refactored, tag @builderio-bot in the PR and say what you want, like move this to a new file or extract this into a hook. The bot replies with updates, and you merge when you are happy.

In practice this has been a great workflow. It feels like the best parts of a game engine editor, with all of the powers of an LLM, but aimed at production web code.

Try out Builder.io for free and I’d love to hear your feedback.

Generate clean code using your components & design tokens
Try FusionGet a demo

Share

Twitter / X
LinkedIn
Facebook
Share this blog
Copy icon
Twitter "X" icon
LinkedIn icon
Facebook icon

Visually edit your codebase with AI

Using simple prompts or Figma-like controls.

Try it nowGet a demo

Design to Code Automation

A pragmatic guide for engineering leaders and development teams

Access Now

Continue Reading
AI6 MIN
Create a 3d scrolling animation with GSAP and Veo 3
October 1, 2025
AI6 MIN
Faster UX Design with AI
October 1, 2025
AI7 MIN
Five things to try with the Supabase MCP server
September 30, 2025