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


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 effectintensity={0.6}
keeps that lighting subtle and not too harshpreset="rembrandt"
adds some classic artistic shadowingadjustCamera={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 speedzoom={0.7}
keeps us at the perfect distance to see all those toppingspolar={[-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. Theenvironment="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 statesRemember 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)! π
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!