Building an Interactive 3D Phone with CSS & Framer Motion

Building an Interactive 3D Phone with CSS & Framer Motion

Saurabh Sharma avatarSaurabh Sharma
April 21, 2026
|
6 min read
ReactFramer MotionTailwind CSS3DUI/UX

In this post, we'll build a 3D mobile phone completely from scratch — just layered divs, CSS transforms, and Framer Motion spring physics. Each section adds one new concept, and you can hover the live previews to see what the code produces at every stage.


Setting Up the 3D Scene

By default, the browser treats everything as flat. Rotate a div and it just squashes sideways — there's no depth. To fix that, we need two things on the parent container:

  • perspective: 1200px — This creates a virtual camera 1200px away from the element. It's what gives objects that natural "closer = bigger" foreshortening effect.
  • transform-style: preserve-3d — Without this, the browser flattens all children back to 2D, even if the parent has perspective. This tells it: keep my children in 3D space.

With those two properties, even a simple rectangle starts to feel like a real object:

<div className="flex justify-center my-8 bg-neutral-950 p-12 rounded-xl border border-neutral-800 [perspective:1200px]"> <motion.div className="w-16 h-32 bg-neutral-700 border-[2px] border-neutral-500 rounded-2xl" animate={{ rotateX: 60, rotateZ: 45 }} /> </div>

Giving It Thickness

Right now our phone is paper-thin. HTML doesn't have a depth property, so we have to fake it — by stacking several identical divs on top of each other and nudging each one slightly backward along the Z-axis.

Think of it like layering sheets of cardboard. Each layer is just 1px deeper than the last. When the 3D rotation kicks in, the browser renders all those tiny edges, and your eye reads them as a solid slab.

Notice how the color shifts from neutral-700 to neutral-800 as the layers go deeper — this subtle gradient sells the illusion of a beveled edge catching light differently:

{/* Stacking multiple divs along the Z-axis to create thickness */} <div className="absolute inset-0 bg-neutral-700 border-[2px] border-neutral-600 rounded-2xl [transform:translateZ(0px)]" /> <div className="absolute inset-0 bg-neutral-700 border-neutral-600 rounded-2xl [transform:translateZ(-1px)]" /> <div className="absolute inset-0 bg-neutral-700 border-neutral-600 rounded-2xl [transform:translateZ(-2px)]" /> <div className="absolute inset-0 bg-neutral-800 border-neutral-700 rounded-2xl [transform:translateZ(-3px)]" /> <div className="absolute inset-0 bg-neutral-800 border-neutral-700 rounded-2xl [transform:translateZ(-4px)]" /> <div className="absolute inset-0 bg-neutral-800 border-[2px] border-neutral-700 rounded-2xl [transform:translateZ(-5px)]" />

Making It Flip on Hover

Now for the fun part. Before we start decorating the screen, let's wire up the interaction that brings this whole thing to life.

The idea is dead simple: track whether the user's mouse is over the container, then swap between two sets of 3D coordinates. When unhovered, the phone sits tilted face-down on the "table." On hover, it springs upright and faces the camera.

The key detail here is the rotateY: 180 in the resting state — that's what makes the phone face away from you by default. On hover, everything resets to 0, flipping it toward you:

const [isHovered, setIsHovered] = useState(false); return ( <div onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > <motion.div initial={false} animate={isHovered ? { rotateX: 0, rotateZ: 0, rotateY: 0 } : { rotateX: 60, rotateZ: 45, rotateY: 180 }} transition={{ type: "spring", bounce: 0.3 }} /> </div> )

The bounce: 0.3 is what makes it feel physical — the phone slightly overshoots its target rotation and settles back, like something with actual mass. Without it, the motion feels robotic.


Building the Screen UI

We've got a solid block that flips. Time to make it actually look like a phone.

The front screen is a new div placed at translateZ(1px) — sitting just above the thickness layers. We add backface-visibility: hidden so it disappears when the phone flips around (otherwise you'd see the UI rendered backwards through the back).

Inside, it's all standard layout work — a notch at the top, some chat-bubble-shaped rectangles to mimic a messaging app, and a rounded input bar at the bottom:

{/* Front Screen */} <div className="absolute inset-0 bg-neutral-950 border-[2px] border-neutral-700 rounded-2xl [backface-visibility:hidden] [transform:translateZ(1px)] flex flex-col overflow-hidden shadow-2xl"> <div className="w-6 h-2 bg-neutral-900 mx-auto rounded-b-md mt-0.5 z-10" /> <div className="flex-1 m-1 p-1 flex flex-col gap-1 justify-end"> <div className="w-2/3 h-3 bg-neutral-800 rounded-md rounded-tl-sm self-start opacity-50" /> <div className="w-3/4 h-3.5 bg-neutral-700 rounded-md rounded-tr-sm self-end" /> <div className="w-1/2 h-3 bg-neutral-800 rounded-md rounded-tl-sm self-start opacity-70" /> <div className="w-4/5 h-4 bg-neutral-700 rounded-md rounded-tr-sm self-end" /> </div> <div className="h-5 mx-1 mb-1.5 bg-neutral-800 rounded-full flex items-center px-1.5"> <div className="w-2 h-2 rounded-full bg-neutral-600" /> </div> </div>

Adding the Back Panel & Camera

If you hover the preview above and flip the phone, you'll notice the back is hollow. Let's fix that.

We place one more div at the very bottom of our Z-stack (translateZ(-6px)) and rotate it 180deg on the Y-axis so it faces backward. This gives us a solid rear surface.

But a flat back looks boring. Real phones have camera bumps — so we build one. A small rounded container holds two circular "lenses" made from nested divs with dark backgrounds and subtle inner shadows. A tiny bright circle acts as the LED flash, and a near-invisible dot simulates a LiDAR sensor. It's all just circles and borders, but at this scale the details really sell it:

{/* Detailed Back Panel */} <div className="absolute inset-0 bg-neutral-800 border-[2px] border-neutral-700 rounded-2xl [backface-visibility:hidden] [transform:translateZ(-6px)_rotateY(180deg)] p-1.5 shadow-xl"> <div className="absolute top-1.5 left-1.5 w-6 h-6 bg-neutral-700/80 rounded-[6px] border border-neutral-600/50 shadow-sm"> {/* Twin Lens */} <div className="absolute top-[2px] left-[2px] w-[9px] h-[9px] rounded-full bg-neutral-950 border border-neutral-800 flex items-center justify-center shadow-inner"> <div className="w-1 h-1 rounded-full bg-purple-900/40" /> </div> <div className="absolute bottom-[2px] left-[2px] w-[9px] h-[9px] rounded-full bg-neutral-950 border border-neutral-800 flex items-center justify-center shadow-inner"> <div className="w-1 h-1 rounded-full bg-purple-900/40" /> </div> {/* LED Flash and LiDAR */} <div className="absolute top-1/2 -translate-y-1/2 right-[3px] w-1.5 h-1.5 rounded-full bg-neutral-200/50 shadow-[0_0_2px_rgba(255,255,255,0.6)]" /> <div className="absolute top-1 right-[5px] w-0.5 h-0.5 rounded-full bg-neutral-950" /> </div> <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-5 h-5 rounded-full border border-neutral-600/30 flex items-center justify-center"> <div className="w-2 h-2 rounded-full bg-neutral-700/50" /> </div> </div>

Putting It All Together

That's the whole phone — assembled one layer at a time. In the final version, a few finishing touches bring it to life:

  • Dark environment — a dimmed background makes the glowing screen pop against the darkness.
  • Floor shadow — a blurred div underneath the phone shrinks and fades as the phone lifts on hover, mimicking how real shadows behave when an object moves away from a surface.
  • Scale on hover — the phone grows slightly when you interact with it, drawing your eye in.

Here's the final result in action:

Curious about what's happening under the hood — the GPU compositing, matrix interpolation, and how Framer Motion's spring solver actually works?
👉 Read the technical deep-dive