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`.

By Satish Kumar October 25, 2025

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:

  1. A Scene Container: To apply perspective.
  2. A Cube Wrapper: This element will have transform-style: preserve-3d and will be the single element we rotate to spin the entire cube.
  3. 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>
  );
}
Front
Back
Left
Right
Top
Bottom

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:

  1. 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).
  2. 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>
Front
Back
Left
Right
Top
Bottom

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>
Front
Back
Left
Right
Top
Bottom

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>
Front
Back
Right
Left
Top
Bottom

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>
Front
Back
Right
Left
Top
Bottom

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>
Front
Back
Right
Left
Top
Bottom

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>
Front
Back
Right
Left
Top
Bottom

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.

Front
Back
Right
Left
Top
Bottom
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.

Front
Back
Right
Left
Top
Bottom
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.

Front
Back
Left
Right
Top
Bottom

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

  1. Complex shapes are built from simple planes. The cube is just six divs with specific transform properties.
  2. The "Orient then Push" Model: The correct transform order is almost always rotate() first, followed by translateZ(). This ensures the element moves "forward" from its new orientation.
  3. rotateX and rotateY are your primary construction tools for positioning walls, floors, and ceilings in 3D space.
  4. Animating the Parent: To animate a complex object, apply the animation (transition, hover:, animate-) to the parent container that has transform-style: preserve-3d.

End-of-Article Challenge

You have successfully built a cube. Now, let's see if you can modify the blueprint.

Front
Back
Left
Right
Top
Bottom

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.


Subscribe to my free Newsletter

Join my weekly newsletter to get the latest updates on design engineering, new projects, and articles I've written. No spam, unsubscribe at any time.