Skip to content
← components

Isometric Cube

new

An isometric 3D cube in pure CSS transforms — three faces tinted from one base color (lighter top, base front, darker right), optional logo on top, optional slow spin. Extracted from explorer.b3.fun.

color
size
96px
spin
logo

Usage

example.tsx
import { Cube } from "~/components/3d/cube";
<Cube color="#3b82f6" size={72} spin />
<Cube color="#8b5cf6" imageUrl="/logo.svg" />

Three faces, one color

A cube only needs three faces if you're looking at one corner. Each is a square rotated to its plane and pushed out by half the cube's size with translateZ, all inside a preserve-3d parent tilted to an isometric angle. No library, no canvas, just transforms.

The nice touch is the shading. Instead of picking three colors, it takes one hex and derives the faces by nudging lightness in HSL: the top is brightest, the front sits in the middle, and the right face stays the base color as the shadowed side. So you pass a single brand color and the cube looks lit. Add spin and a keyframe rotates the parent a full turn on Y.

cube.tsx
// three faces, each rotated out and pushed half the cube forward
const half = size / 2;
<div style={{ transformStyle: "preserve-3d",
transform: "rotateX(29.264deg) rotateY(-225deg)" }}>
<div style={{ background: front, transform: `rotateY(0deg) translateZ(${half}px)` }} />
<div style={{ background: right, transform: `rotateY(90deg) translateZ(${half}px)` }} />
<div style={{ background: top, transform: `rotateX(90deg) translateZ(${half}px)` }} />
</div>
// face colors come from ONE hex via HSL lightness nudges
const front = hexToHSL(color, 0.22); // lifted
const right = color; // base (the dark side)
const top = hexToHSL(color, 0.30); // brightest

Props

proptypedefaultdescription
colorstringBase hex; faces derive from it.
imageUrlstringOptional logo on the top face.
sizenumber72Cube size in px.
spinbooleanfalseSlowly rotate on the Y axis.

From explorer.b3.fun, a project I work on at B3.

The full implementation, across 2 files. Copy it in or grab the zip. It leans on a cn() class helper and the theme tokens (--primary, --border, …).

components/3d/cube.tsx
import type { CSSProperties } from "react";
import { cn } from "~/lib/utils";
/** Convert a hex color to an HSL string, nudging lightness by `lAdjust` (−1..1). */
function hexToHSL(hex: string, lAdjust = 0): string {
hex = hex.replace("#", "");
let r = 0,
g = 0,
b = 0;
if (hex.length === 3) {
r = parseInt(hex[0] + hex[0], 16);
g = parseInt(hex[1] + hex[1], 16);
b = parseInt(hex[2] + hex[2], 16);
} else if (hex.length === 6) {
r = parseInt(hex.slice(0, 2), 16);
g = parseInt(hex.slice(2, 4), 16);
b = parseInt(hex.slice(4, 6), 16);
}
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b),
min = Math.min(r, g, b);
let h = 0,
s = 0;
let l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
l = Math.min(1, Math.max(0, l + lAdjust));
return `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`;
}
export interface CubeProps {
/** Base hex color; faces are derived from it. */
color: string;
/** Optional logo/image on the top face. */
imageUrl?: string;
size?: number;
/** Slowly rotate on the Y axis. */
spin?: boolean;
className?: string;
}
/**
* An isometric 3D cube in pure CSS transforms — three visible faces, each tinted
* from one base color (lighter top, base front, darker right), with an optional
* logo on top. Extracted from explorer.b3.fun.
*/
export function Cube({ color, imageUrl, size = 72, spin, className }: CubeProps) {
const front = hexToHSL(color, 0.22);
const right = color;
const top = hexToHSL(color, 0.3);
const bottom = hexToHSL(color, -0.1);
const half = size / 2;
const face = (bg: string, transform: string): CSSProperties => ({
position: "absolute",
width: size,
height: size,
background: bg,
transform,
});
return (
<div className={cn("relative flex items-center justify-center", className)}>
<div
className={cn("cube-3d", spin && "cube-3d-spin")}
style={{
width: size,
height: size,
position: "relative",
transformStyle: "preserve-3d",
transform: "rotateX(29.264deg) rotateY(-225deg)",
}}
>
{/* hidden until spun: back / left / bottom complete the cube */}
<div style={face(front, `rotateY(180deg) translateZ(${half}px)`)} />
<div style={face(right, `rotateY(-90deg) translateZ(${half}px)`)} />
<div style={face(bottom, `rotateX(-90deg) translateZ(${half}px)`)} />
<div style={face(front, `rotateY(0deg) translateZ(${half}px)`)} />
<div style={face(right, `rotateY(90deg) translateZ(${half}px)`)} />
<div
style={{
...face(top, `rotateX(90deg) translateZ(${half}px)`),
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{imageUrl && (
<div
style={{
width: size * 0.6,
height: size * 0.6,
transform: "rotate(180deg)",
backgroundImage: `url("${imageUrl}")`,
backgroundSize: "contain",
backgroundRepeat: "no-repeat",
backgroundPosition: "center",
}}
/>
)}
</div>
</div>
</div>
);
}
app/styles/app.css
/* ── Isometric CSS cube spin, from explorer.b3.fun ───────────────────────── */
.cube-3d-spin {
animation: cube-spin 9s linear infinite;
}
@keyframes cube-spin {
from { transform: rotateX(29.264deg) rotateY(-225deg); }
to { transform: rotateX(29.264deg) rotateY(135deg); }
}
@media (prefers-reduced-motion: reduce) {
.cube-3d-spin { animation: none; }
}