3D CSS Box
A simple 3D box component with "CSS-only" 3D transforms.
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
- Two motion values
baseRotateX
andbaseRotateY
hold the raw rotation in degrees. - They are piped through
useSpring
so they feel springy and configurable (stiffness
,damping
). See Motion – useSpring for more details. - 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 anglesgetCurrentRotation()
– 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:
Or, tie the rotation to a scroll progress:
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
- Intro to CSS 3D transforms by David DeSandro
- Gimbal Lock
- Quaternions explained by 3Blue1Brown
Props
Prop | Type | Default | Description |
---|---|---|---|
width* | number | - | Width of the cube (in px) |
height* | number | - | Height of the cube (in px) |
depth* | number | - | Depth of the cube (in px) |
perspective | number | 600 | Perspective distance applied to the outer wrapper |
stiffness | number | 100 | Spring stiffness for rotations |
damping | number | 30 | Spring damping factor |
className | string | - | Additional classes for the outer wrapper |
showBackface | boolean | false | Reveal back-faces if you need double-sided content |
faces | { front? back? left? right? top? bottom?: ReactNode } | - | Individual React nodes for every face |
draggable | boolean | true | Enable/disable mouse & touch rotation |
Ref Methods
The component exposes several methods through a ref that allow programmatic control of the cube's rotation:
Method | Type | Description |
showFront | () => void | Rotates the cube to show the front face (0°, 0°) |
showBack | () => void | Rotates the cube to show the back face (0°, 180°) |
showLeft | () => void | Rotates the cube to show the left face (0°, -90°) |
showRight | () => void | Rotates the cube to show the right face (0°, 90°) |
showTop | () => void | Rotates the cube to show the top face (-90°, 0°) |
showBottom | () => void | Rotates the cube to show the bottom face (90°, 0°) |
rotateTo | (x: number, y: number) => void | Rotates the cube to specific X and Y angles in degrees |
getCurrentRotation | () => { x: number, y: number } | Returns current X and Y rotation angles in degrees |