Docs
3D CSS Box

3D CSS Box

A simple 3D box component with "CSS-only" 3D transforms.

YOU CAN
JUST
DO THINGS
MAKE THINGS
YOU WISH
EXISTED
MAKE THINGS
YOU WISH
EXISTED
BREAK
THINGS
MOVE
FAST
YOU CAN
JUST
DO THINGS
BREAK
THINGS
MOVE
FAST

Artwork inspiration from Ignite Amsterdam

Credits


The component is derived from the Box chapter of David De Sandro's extremely awesome Intro to CSS 3D transforms tutorial.

Installation


npx shadcn@latest add "https://fancycomponents.dev/r/3d-css-box.json"

Usage


The component renders a fully-featured 3D cube. Pass width, height, depth and optionally six React nodes for the faces. You may also grab the cube with a ref for programmatic control.

High-level example:

import { useRef } from "react"
import CSSBox, { CSSBoxRef } from "@/components/blocks/css-box"

export default function CubeExample() {
  const cubeRef = useRef<CSSBoxRef>(null)

  return (
    <>
      <CSSBox
        ref={cubeRef}
        width={220}
        height={220}
        depth={220}
        perspective={800}
        draggable
        faces={{
          front:  <img src="/images/front.png"  alt="Front"  />,
          back:   <img src="/images/back.png"   alt="Back"   />,
          left:   <img src="/images/left.png"   alt="Left"   />,
          right:  <img src="/images/right.png"  alt="Right"  />,
          top:    <img src="/images/top.png"    alt="Top"    />,
          bottom: <img src="/images/bottom.png" alt="Bottom" />,
        }}
      />

      <Button onClick={() => cubeRef.current?.showTop()}>
        Show Top
      </Button>
    </>
  )
}

Understanding the component


Before you dive into it, I highly recommend reading Intro to CSS 3D transforms by David DeSandro. It's a really great resource for understanding the basics, and this component is essentially just a react & tailwind port of the Box chapter.

Face layout

As you know, a box is a 3D object that has six faces. Each face is an absolutely-positioned <div> that lives in the same 3D context (transform-style: preserve-3d).
We pre-rotate every face so that their local +Z axis points outward and then translate it by half of the appropriate dimension:

rotateY( 0deg) translateZ(depth / 2) → front
rotateY(180deg) translateZ(depth / 2) → back
rotateY( 90deg) translateZ(width / 2) → right
rotateY(-90deg) translateZ(width / 2) → left
rotateX( 90deg) translateZ(height/ 2) → top
rotateX(-90deg) translateZ(height/ 2) → bottom

Rotation mechanics

  1. Two motion values baseRotateX and baseRotateY hold the raw rotation in degrees.
  2. They are piped through useSpring so they feel springy and configurable (stiffness, damping). See Motion – useSpring for more details.
  3. We combine them into a single CSS transform:
const transform = useTransform([springX, springY], ([x, y]) =>
  `translateZ(-${depth / 2}px) rotateX(${x}deg) rotateY(${y}deg)`
)

Drag interaction

The box can be rotated through mouse drags or touch input. 3D rotation can be a nasty thing, especially when dealing with Gimbal Lock. While the almighty, super complex quaternions could prevent this issue (Three.js provides great utilities for that), implementing them felt like overkill here - at that point, the entire box might as well be rendered in Three.js.

The current approach maps mouse/touch movement directly to rotation around the X and Y axes. The implementation is pretty intuitive, while the actual feel of it can be sometimes unintuitive. Apologies for my laziness here.

When draggable is enabled, pointer movement gets translated into smooth rotational changes:

Δx → rotateY
Δy → rotateX

We do this by subscribing to mousemove and touchmove events and projecting the movement to rotation deltas. During dragging the spring’s stiffness is temporarily halved to give a slightly “looser” feel.

baseRotateX.set(startRotation.current.x - deltaY / 2)
baseRotateY.set(startRotation.current.y + deltaX / 2)

Modify that value to adjust the sensitivity of the drag.

Imperative API

Via ref you can trigger the following methods:

  • showFront | showBack | showLeft | showRight | showTop | showBottom
  • rotateTo(x: number, y: number) – set exact angles
  • getCurrentRotation() – read the live values

This can be handy for syncing cube state to a carousel or step-based walkthrough. For example, you can trigger a cube rotation with hover:

January 15, 2025
January 15, 2025
January 15, 2025
January 15, 2025
Live Q&A
Live Q&A
Live Q&A
Live Q&A
10:00
10:00
10:00
10:00
to
to
to
to
11:30
11:30
11:30
11:30
CET
CET
CET
CET
Online
Online
Online
Online
Recording Available
Recording Available
Recording Available
Recording Available
In English
In English
In English
In English
Register Now
Register Now
Register Now
Register Now
Free Access
Free Access
Free Access
Free Access

Or, tie the rotation to a scroll progress:

JUN14
New Arrivals
SS25
JUN14
New Arrivals
SS25
JUN14
New Arrivals
SS25
JUN14
New Arrivals
SS25

Notes


As it was pointed out above, implementing a similar component in Three.js would have been a lot easier and would give you much more flexibility and overall control over the rotation. You are still welcomed to use this component if you'd like to skip installing Three.js for whatever reason :).

Resources


Props


PropTypeDefaultDescription
width*number-Width of the cube (in px)
height*number-Height of the cube (in px)
depth*number-Depth of the cube (in px)
perspectivenumber600Perspective distance applied to the outer wrapper
stiffnessnumber100Spring stiffness for rotations
dampingnumber30Spring damping factor
classNamestring-Additional classes for the outer wrapper
showBackfacebooleanfalseReveal back-faces if you need double-sided content
faces{ front? back? left? right? top? bottom?: ReactNode }-Individual React nodes for every face
draggablebooleantrueEnable/disable mouse & touch rotation

Ref Methods


The component exposes several methods through a ref that allow programmatic control of the cube's rotation:

MethodTypeDescription
showFront() => voidRotates the cube to show the front face (0°, 0°)
showBack() => voidRotates the cube to show the back face (0°, 180°)
showLeft() => voidRotates the cube to show the left face (0°, -90°)
showRight() => voidRotates the cube to show the right face (0°, 90°)
showTop() => voidRotates the cube to show the top face (-90°, 0°)
showBottom() => voidRotates the cube to show the bottom face (90°, 0°)
rotateTo(x: number, y: number) => voidRotates the cube to specific X and Y angles in degrees
getCurrentRotation() => { x: number, y: number }Returns current X and Y rotation angles in degrees