Making Your First 3D Web Animation with THREE.js and Framer-Motion (It's as Easy as Pie!) πŸ•

Samir ShumanSamir Shuman
6 min read

Remember that pizza-filled text from my last post? Well, today we're taking that delicious inspiration to a whole new dimension. The third dimension to be exact! Instead of just filling text with pizza, we're going to be cooking up an extra-large, interactive, 3D pizza in React. If you thought that filling text with gradients and custom background images was cool, wait until you see what we can do with React Three Fiber! 🎨

React Three Fiber is a React renderer for Three.js, which lets us use JSX to build 3D scenes declaratively. It makes Three.js easier to use in React projects.

One of the key techniques we'll be using throughout this example is the combination of Framer Motion and React Three Fiber. Framer Motion is a powerful animation library that brings fluid motion and gestures to React applications. Think of React Three Fiber as the dough that makes our 3D effect possible, and Framer Motion as the toppings that make it delicious... I mean, animated! (Can you tell I'm still thinking about that pizza text effect? πŸ˜‹)

Before We Start πŸŽ“

You'll want to be familiar with:

  • React basics (components, hooks)

  • JavaScript fundamentals

  • A code editor (VS Code recommended)

Let's dive into making our very own animated 3D pizza! πŸš€

The Basic Setup πŸ› οΈ

First, we need our ingredients (dependencies):

npx create-react-app pizza-3d-demo
cd pizza-3d-demo
npm install three @react-three/fiber @react-three/drei framer-motion

Before I forget, I want to give credit where credit is due. The amazing pizza 3D model we'll be using is: Pepperoni pizza by Poly by Google [CC-BY] (https://creativecommons.org/licenses/by/3.0/) via Poly Pizza (https://poly.pizza/m/9IWGn64Fnqo)

The Magic Component ✨

We're going to create a component that loads and animates our 3D pizza model:

import { useGLTF } from '@react-three/drei';
import { motion } from 'framer-motion-3d';
import { useEffect } from 'react';

export function Pizza3D() {
  const pizza = useGLTF('/models/pepperoni_pizza.glb');

  useEffect(() => {
    return () => {
      if (pizza) pizza.scene.dispose();
    };
  }, [pizza]);

  return (
    <motion.group
      initial={{ 
        scale: 0, 
        rotateX: -50,
        x: 0,
        y: 200, 
      }}
      animate={{
        scale: 1,
        rotateX: 0,
        x: 0,
        y: 0,
        transition: {
          type: "spring",
          stiffness: 100, 
          damping: 8,    
          mass: 1,       
        }
      }}
      whileHover={{
        scale: 1.1,
        rotateX: 0.2,
        rotateZ: 0.2,
        y: 1.5,
        transition: {
          type: "tween",
          duration: 0.2
        }
      }}
    >
      <primitive 
        object={pizza.scene} 
        scale={1.5}
        position={[0, 0, 0]}
      />
    </motion.group>
  );
}

Just like how we used background-clip: text to create a mask for our text effects, here we're using motion.group to create a container for our 3D animations. It’s like a pizza box that we use to move our pizza around.

The useEffect hook is here for clean up. It makes sure we properly dispose of the 3D pizza model when we’re done with it. It’s kind of like taking out the trash after a pizza party. Without this, we could potentially have memory leaks, which nobody wants.

Setting the Stage 🎭

Now, let's create the perfect environment for our pizza to perform. Just like setting up the perfect shot for your Instagram food pics, we need to create the right environment for our 3D pizza to shine. The Canvas component acts as our viewfinder, while Stage handles all the fancy lighting setup.

import { Canvas } from '@react-three/fiber';
import { OrbitControls, PresentationControls, Stage } from '@react-three/drei';
import { Pizza3D } from './Pizza3D';

export function Scene() {
  return (
    <div style={{ width: '100vw', height: '100vh' }}>
      <Canvas>
        <Stage 
          environment="city" 
          intensity={0.6}
          preset="rembrandt"
          adjustCamera={1.2}
        >
          <PresentationControls
            speed={1.5}
            global
            zoom={0.7}
            polar={[-0.1, Math.PI / 4]}
          >
            <Pizza3D />
          </PresentationControls>
        </Stage>
        <OrbitControls enableZoom={false} />
      </Canvas>
    </div>
  );
}

The Stage component is doing all the heavy lifting behind the scenes here:

  • That environment="city" prop adds a nice lighting effect

  • intensity={0.6} keeps that lighting subtle and not too harsh

  • preset="rembrandt" adds some classic artistic shadowing

  • adjustCamera={1.2} makes sure our pizza is perfectly framed in view

I also wrapped our pizza in PresentationControls to give it that smooth, responsive feel when users interact with it. You can think of it as a turntable that lets us show the pizza from all of its cheesy angles. The props here control exactly how it moves:

  • speed={1.5} gives it a smooth movement speed

  • zoom={0.7} keeps us at the perfect distance to see all those toppings

  • polar={[-0.1, Math.PI / 4]} makes sure users can't rotate it at weird angles

And finally, I added OrbitControls with enableZoom={false} to keep folks from getting too close to those crispy pepperonis! πŸ•

The Animation Magic 🎩

Just like how we used keyframes for our neon text effect, we're using Framer Motion's animation properties to bring our pizza to life. But instead of just changing colors, we're creating a dramatic entrance in 3D space.

The entrance animation has our pizza making a grand entrance:

  • Starts floating high above (y: 200)

  • Has a slight tilt (rotateX: -50)

  • Falls gracefully with spring physics

And when you hover, the pizza does a little happy dance:

  • Lifts up slightly (y: 1.5)

  • Grows a bit (scale: 1.1)

  • Tilts playfully (rotateX: 0.2, rotateZ: 0.2)

We've also added a sliding title animation for extra pizzazz, because who doesn't love a dramatic entrance? The title starts completely off-screen to the left (-100vw) and slides into the middle of the viewport (-50vw):

<motion.h1
  className="title"
  initial={{ x: '-100vw' }}
  animate={{ x: '-50vw' }}
  transition={{
    type: 'spring',
    stiffness: 100,
    damping: 25,
    duration: 3,
  }}
>
  THREE.js and Framer-Motion <br />
  <span className="image-text">P</span>
  <span className="image-text">I</span>
  <span className="image-text">Z</span>
  <span className="image-text">Z</span>
  <span className="image-text">A</span> Demo
</motion.h1>

Each letter of "PIZZA" gets its own span with the image-text class which we can then style separately (more on that here). And just like our pizza animation, we're using spring physics for that bouncy, playful feel. The stiffness and damping values give it just the right amount of bounce without going overboard! 🎯

Cowabunga, Dude! πŸ•πŸ’

Key Insights πŸ”

The 3D transformation trick: Just like how making text transparent was crucial for our gradient effects, using motion.group is essential for 3D animations. It creates a container that can be animated in 3D space without affecting the model itself.

  • Physics-Based Animation: The combination of spring physics (stiffness: 100, damping: 8) with added mass creates a satisfyingly bouncy entrance that makes the pizza feel alive and playful.

  • Lighting and Environment: The Stage component is like a photo studio. It provides perfect lighting to make the pizza look delicious from every angle. The environment="city" prop gives nice reflective highlights.

  • Interactive Considerations: Remember how pseudo-elements were used for text shadows? Here, PresentationControls handle user interaction. It ensures the pizza rotates smoothly and naturally.

Making It Responsive πŸ“±

Just like our text effects, we want this to work everywhere:

.scene-container {
  width: 100vw;
  height: clamp(300px, 100vh, 800px);
}

Let's See What You Create!

Now it's your turn! Try playing with different:

  • Animation timings πŸ•°οΈ

  • Spring physics settings πŸŒ€

  • Camera angles πŸ“Έ

  • Lighting setups πŸ’‘

Drop a comment below with your creations! You can also hit me up on BlueSky, X/Twitter, or on my website, devmansam.net, if you come up with something cool!
(You can also contact me at linktr.ee/devmansam)

Thanks for stopping by. πŸ˜ŽπŸ€™πŸ½

Key Tips for Those New to 3D:

  • Keep your models lightweight

  • Test on mobile devices

  • Use Suspense for loading states

  • Remember to clean up your models with useEffect

  • For more on Framer-Motion visit motion.dev

  • Documentation for React Three Fiber can be found here

Want to learn how to create that pizza-filled text effect I mentioned at the start? Check out my previous blog post where I show you how to make it step by step!

Happy coding (and don't forget to grab a real slice of pizza when you’re done)! πŸ•

3
Subscribe to my newsletter

Read articles from Samir Shuman directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Samir Shuman
Samir Shuman

Welcome to my blog! My name is Samir. I'm a full-stack software engineer specializing in web development. I'm based in the San Francisco Bay Area, but can deliver digital solutions globally. When I'm not coding, I love experimenting in the kitchen, watching basketball, and spending time with my cat, Fuzzy. Let's connect!