Assembling a 3D Object - The CSS Cube
Learn to construct a complete, volumetric 3D cube from multiple flat planes using CSS. This step-by-step guide covers the logic of using `rotate` and `translateZ`.
Part of the series: Unlocking CSS 3D
- Part 1The Mental Model - Before You Write a Line of CSS 3D
- Part 2The Foundation - Mastering 2D Transforms
- Part 3Entering the Third Dimension - The Flipping Card
- Part 4Assembling a 3D Object - The CSS Cube
- Part 5The Final Polish - Interactivity, Lighting, and Performance
- Part 6The Real World - Practical 3D Components
We have journeyed from the flat plane of 2D into the exciting realm of 3D space. In the last article, we mastered the art of creating a 3D scene, manipulating an object around a single axis to build a flipping card. We learned the critical relationship between perspective and transform-style: preserve-3d.
Now, it is time to become a true architect in this new dimension. Our goal is no longer to just flip a plane, but to construct a complete, volumetric object from multiple flat planes. This article will guide you, step-by-step, through building the most iconic of all CSS 3D creations: a rotating cube.
Mastering this will solidify your understanding of the 3D coordinate system and give you the skills to build any multi-faceted 3D shape you can imagine.
The Blueprint: A Cube is Just Six Planes in Space
The core concept is surprisingly simple. A 3D cube is not a magical, single entity in CSS. It is an illusion created by taking six regular, two-dimensional divs and applying a unique transform to each one, precisely positioning them in 3D space.
Our structure will be similar to the flipping card, but expanded:
- A Scene Container: To apply
perspective. - A Cube Wrapper: This element will have
transform-style: preserve-3dand will be the single element we rotate to spin the entire cube. - Six Face Elements: Each will be a
div, absolutely positioned at the center of the wrapper, waiting for its unique transform.
Let's lay out the initial HTML structure in our Next.js project.
Create this below component CubeFace.
const CubeFace = ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
return (
<div
className={`
absolute w-48 h-48 flex items-center justify-center
border-2 border-chart-1-400/50 bg-chart-1-900/30
text-2xl font-bold text-foreground-300
${className}
`}
>
{children}
</div>
);
};Create this below component to Create our Cube.
import { CubeFace } from './cube-face';
export default function Cube() {
return (
<main className='min-h-screen bg-background text-foreground flex justify-center items-center'>
<div className='[perspective:1000px]'>
{/* The Cube Wrapper */}
<div className='relative w-48 h-48 [transform-style:preserve-3d]'>
<CubeFace>Front</CubeFace>
<CubeFace>Back</CubeFace>
<CubeFace>Left</CubeFace>
<CubeFace>Right</CubeFace>
<CubeFace>Top</CubeFace>
<CubeFace>Bottom</CubeFace>
</div>
</div>
</main>
);
}Right now, this looks like a mess—just six divs stacked directly on top of each other. Our job is to give each one the correct transform to move it into its final position.
The Logic of Placement: rotate then translateZ
This is the most crucial part of the process. For each face, we will follow a two-step mental model:
- Orient (Rotate): First, we turn the face so it's pointing in the correct direction (e.g., we turn the "right" face 90 degrees to the right).
- Push/Pull (Translate): Then, we push it "forward" from its new orientation out from the center.
Our cube is 192px wide (w-48 which is 12rem). This means the distance from the center of the cube to the center of any face is half that: 96px. This 96px value will be our magic number for translateZ.
We will use Tailwind's arbitrary property syntax [...] to apply these precise, combined transforms.
Let's build the cube, face by face:
1. The Front Face: This is the easiest. It's already facing the correct direction. We just need to pull it forward.
<CubeFace className='[transform:translateZ(96px)]'>Front</CubeFace>2. The Back Face: First, we need to spin it around 180 degrees so it's facing away from us. Then, we push it forward (from its new perspective) by 96px.
<CubeFace className='[transform:rotateY(180deg)_translateZ(96px)]'>
Back
</CubeFace>Note the underscore "_" in the class name
This is Tailwind's syntax for representing a space within an arbitrary property.
3. The Right Face: We rotate it 90 degrees to the right around the Y-axis, then push it forward.
<CubeFace className='[transform:rotateY(90deg)_translateZ(96px)]'>
Right
</CubeFace>4. The Left Face: We rotate it 90 degrees to the left (a negative rotation) around the Y-axis, then push it forward.
<CubeFace className='[transform:rotateY(-90deg)_translateZ(96px)]'>
Left
</CubeFace>5. The Top Face: Now we introduce a new axis. To make the top face, we need to rotate it upwards around the X-axis (like a hinge along the horizon), then push it forward.
<CubeFace className='[transform:rotateX(90deg)_translateZ(96px)]'>Top</CubeFace>6. The Bottom Face: Finally, we rotate the bottom face downwards around the X-axis and push it forward.
<CubeFace className='[transform:rotateX(-90deg)_translateZ(96px)]'>
Bottom
</CubeFace>The Grand Assembly and Bringing It to Life
Let's put all the pieces together and add a hover animation to the parent div to admire our work.
import { CubeFace } from './cube-face';
export default function Final() {
return (
<main className='h-64 bg-background text-foreground flex justify-center items-center'>
<div className='group [perspective:1000px]'>
{/* Add group for hover and animation */}
<div
className='relative w-48 h-48 [transform-style:preserve-3d]
transition-transform duration-1000
group-hover:[transform:rotateY(120deg)_rotateX(-30deg)]'
>
<CubeFace className='[transform:translateZ(96px)]'>Front</CubeFace>
<CubeFace className='[transform:rotateY(180deg)_translateZ(96px)]'>
Back
</CubeFace>
<CubeFace className='[transform:rotateY(90deg)_translateZ(96px)]'>
Right
</CubeFace>
<CubeFace className='[transform:rotateY(-90deg)_translateZ(96px)]'>
Left
</CubeFace>
<CubeFace className='[transform:rotateX(90deg)_translateZ(96px)]'>
Top
</CubeFace>
<CubeFace className='[transform:rotateX(-90deg)_translateZ(96px)]'>
Bottom
</CubeFace>
</div>
</div>
</main>
);
}Add some solid color to cube face to get a good viewing experience.
export const CubeFace = ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
return (
<div
className={`
absolute w-48 h-48 flex items-center justify-center
border-4 border-orange-600 bg-yellow-500
text-2xl font-bold text-foreground-300
${className}
`}
>
{children}
</div>
);
};Now, when you load the page, you'll see a fully formed 3D cube. When you hover over it, the entire Cube Wrapper div will smoothly rotate, showing off the different faces. You've successfully constructed a volumetric object from flat planes.
Interactive Playground: The Cube Assembler
Objective: To demystify the construction process by allowing the user to build the cube piece-by-piece.
Assemble the Cube
Code for the Playground Component:
'use client';
import { useState } from 'react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
const faces = [
{ name: 'Front', transform: 'translateZ(72px)' },
{ name: 'Back', transform: 'rotateY(180deg) translateZ(72px)' },
{ name: 'Left', transform: 'rotateY(-90deg) translateZ(72px)' },
{ name: 'Right', transform: 'rotateY(90deg) translateZ(72px)' },
{ name: 'Top', transform: 'rotateX(90deg) translateZ(72px)' },
{ name: 'Bottom', transform: 'rotateX(-90deg) translateZ(72px)' },
];
export const CubeAssemblerPlayground = () => {
const [visibleFaces, setVisibleFaces] = useState<string[]>([]);
const toggleFace = (name: string) => {
setVisibleFaces((prev) =>
prev.includes(name) ? prev.filter((f) => f !== name) : [...prev, name]
);
};
return (
<div className={cn('w-full p-6 grid md:grid-cols-2 gap-16')}>
<div className='[perspective:800px] flex items-center justify-center w-full'>
<div
className='relative w-36 h-36 [transform-style:preserve-3d] transition-transform duration-1000 animate-spin-slow'
style={{ animation: 'spin 15s linear infinite' }}
>
{faces.map((face) => (
<div
key={face.name}
className={cn(`absolute w-36 h-36 flex items-center justify-center
border bg-accent text-accent-foreground text-sm font-bold rounded-lg
transition-opacity duration-500`)}
style={{
transform: face.transform,
opacity: visibleFaces.includes(face.name) ? 1 : 0,
}}
>
{face.name}
</div>
))}
</div>
{/* Keyframes for animation would be in a global CSS file or a style tag */}
<style jsx global>{`
@keyframes spin {
from {
transform: rotateY(0deg) rotateX(0deg);
}
to {
transform: rotateY(360deg) rotateX(360deg);
}
}
.animate-spin-slow {
animation: spin 15s linear infinite;
}
`}</style>
</div>
<div className='space-y-2 flex flex-col justify-center items-center'>
<p className='text-center font-semibold text-foreground'>
Assemble the Cube
</p>
{faces.map((face) => (
<Button
key={face.name}
variant={visibleFaces.includes(face.name) ? 'default' : 'secondary'}
size='sm'
onClick={() => toggleFace(face.name)}
>
{visibleFaces.includes(face.name) ? 'Hide' : 'Show'} {face.name}
</Button>
))}
<Button
variant='destructive'
size='sm'
onClick={() => setVisibleFaces([])}
className='mt-4'
>
Reset
</Button>
</div>
</div>
);
};Key Takeaways
- Complex shapes are built from simple planes. The cube is just six
divs with specifictransformproperties. - The "Orient then Push" Model: The correct transform order is almost always
rotate()first, followed bytranslateZ(). This ensures the element moves "forward" from its new orientation. rotateXandrotateYare your primary construction tools for positioning walls, floors, and ceilings in 3D space.- Animating the Parent: To animate a complex object, apply the animation (
transition,hover:,animate-) to the parent container that hastransform-style: preserve-3d.
End-of-Article Challenge
You have successfully built a cube. Now, let's see if you can modify the blueprint.
rectangular Prism
Your Challenge: Create a rectangular prism, like a shoebox or a domino. It should be taller than it is wide or deep.
Hints:
- You will need to use different
w-*andh-*classes for the different faces. The front/back faces will have one size, while the left/right/top/bottom faces will have another. - The "magic number" for
translateZwill no longer be the same for every face. You will need two different values: one for the front/back faces (half the depth) and one for the top/bottom/left/right faces (half the height/width).
This challenge will test your understanding of how the dimensions of the faces relate to their translateZ values. Once you've mastered this, you're ready for our final article where we'll add the polish.