Overlays
Rubik Cube
#cube#3d#interactive
implementedInfinityUI component
This component is fully implemented in InfinityUI and wired into the docs and registry flow.
Installation
Use the registry command to add the component source, and install any package dependencies if needed.
Infinity Registry
Install the component source into your project with the shadcn CLI.
npx shadcn@latest add https://infinityui-pearl.vercel.app/r/rubik-cubePackage Dependencies
Install the npm packages used by this component source.
npm install clsx framer-motion tailwind-mergeComponent Code
Copy and paste this code into your component file.
tsx
"use client";
import { useEffect } from "react";
import {
animate,
motion,
useAnimationControls,
useMotionValue,
useTransform,
} from "framer-motion";
import { cn } from "@/lib/utils";
const size = 48;
const dimensionModes = [
{ mode: "2x2", center: [] as number[] },
{ mode: "3x3", center: [4] },
{ mode: "4x4", center: [5, 6, 9, 10] },
] as const;
function Piece({
config,
index,
mode,
isCenterPiece,
}: {
config: { color: string; position: string };
index: number;
mode: number;
isCenterPiece: boolean;
}) {
const getGlassColor = (pieceIndex: number) => {
const colors = [
"rgba(255, 255, 255, 0.15)",
"rgba(173, 216, 230, 0.2)",
"rgba(240, 248, 255, 0.18)",
"rgba(230, 230, 250, 0.16)",
"rgba(255, 250, 240, 0.17)",
"rgba(245, 245, 245, 0.19)",
"rgba(248, 248, 255, 0.14)",
"rgba(250, 235, 215, 0.15)",
"rgba(255, 245, 238, 0.16)",
];
return colors[pieceIndex % colors.length];
};
return (
<motion.div
style={{ height: size, width: size }}
animate={{
backgroundColor:
mode === 2
? getGlassColor(index)
: mode === 0
? "rgba(0, 0, 0, 0)"
: config.color,
borderColor:
mode === 0
? index % 2 === 0
? "rgba(220, 220, 220, 1)"
: "rgba(200, 200, 200, 1)"
: mode === 2
? "rgba(255, 255, 235, 1)"
: "#000000",
backdropFilter: mode === 2 ? "blur(4px)" : "none",
}}
transition={{ duration: 0.3, ease: "linear", delay: index * 0.05 }}
className={cn("relative shrink-0 border-[1.5px]", isCenterPiece && "z-10")}
>
{isCenterPiece && mode !== 0 && (
<>
<div className="absolute top-0 left-0 z-[1000] h-3 w-3 -translate-x-[calc(50%+2px)] -translate-y-[calc(50%+2px)] rotate-45 bg-black" />
<div className="absolute top-0 right-0 z-[1000] h-3 w-3 translate-x-[calc(50%+2px)] -translate-y-[calc(50%+2px)] rotate-45 bg-black" />
<div className="absolute right-0 bottom-0 z-[1000] h-3 w-3 translate-x-[calc(50%+2px)] translate-y-[calc(50%+2px)] rotate-45 bg-black" />
<div className="absolute bottom-0 left-0 z-[1000] h-3 w-3 -translate-x-[calc(50%+2px)] translate-y-[calc(50%+2px)] rotate-45 bg-black" />
</>
)}
</motion.div>
);
}
function Face({
config,
mode,
dimensionMode,
}: {
config: { color: string; position: string };
mode: number;
dimensionMode: number;
}) {
const displacementMultiplier =
dimensionMode === 0 ? 1 : dimensionMode === 1 ? 1.5 : 2;
const calculatedPosition = {
rotateX: 0,
rotateY: 0,
translateY: 0,
translateZ: 0,
translateX: 0,
};
if (config.position === "top") {
calculatedPosition.rotateX = 90;
calculatedPosition.translateY = -size * displacementMultiplier;
} else if (config.position === "back") {
calculatedPosition.translateZ = size * displacementMultiplier;
} else if (config.position === "front") {
calculatedPosition.translateZ = -size * displacementMultiplier;
} else if (config.position === "bottom") {
calculatedPosition.translateY = size * displacementMultiplier;
calculatedPosition.rotateX = -90;
} else if (config.position === "right") {
calculatedPosition.rotateY = 90;
calculatedPosition.translateX = size * displacementMultiplier;
} else if (config.position === "left") {
calculatedPosition.rotateY = -90;
calculatedPosition.translateX = -size * displacementMultiplier;
}
const pieceCount = dimensionMode === 0 ? 4 : dimensionMode === 1 ? 9 : 16;
return (
<motion.div
style={{
...calculatedPosition,
transformStyle: "preserve-3d",
backfaceVisibility: "visible",
}}
className={cn(
"absolute grid shrink-0",
dimensionMode === 0
? "grid-cols-2"
: dimensionMode === 1
? "grid-cols-3"
: "grid-cols-4"
)}
>
{Array.from({ length: pieceCount }).map((_, index) => (
<Piece
key={`${config.position}-${index}`}
config={config}
index={index}
isCenterPiece={dimensionModes[dimensionMode].center.some(
(centerIndex) => centerIndex === index
)}
mode={mode}
/>
))}
</motion.div>
);
}
export interface RubikCubeProps {
mode?: "skeleton" | "normal" | "glass";
dimensionMode?: "2x2" | "3x3" | "4x4";
}
export function RubikCube({
mode = "normal",
dimensionMode = "3x3",
}: RubikCubeProps) {
const controls = useAnimationControls();
const faceColors = [
{ color: "#C41E3A", position: "left" },
{ color: "#FF5800", position: "right" },
{ color: "#009E60", position: "top" },
{ color: "#0051BA", position: "bottom" },
{ color: "#FFD500", position: "front" },
{ color: "#ffffff", position: "back" },
];
const rotateX = useMotionValue(45);
const rotateY = useMotionValue(45);
const dragRotateX = useTransform(rotateX, (value) => value);
const dragRotateY = useTransform(rotateY, (value) => value);
const handleDragEnd = async () => {
await Promise.all([
animate(rotateX, 45, { duration: 0.8, ease: "easeOut" }),
animate(rotateY, 45, { duration: 0.8, ease: "easeOut" }),
]);
controls.start({
rotateX: [45, 405],
rotateY: [45, -405],
transition: {
duration: 10,
ease: "linear",
repeat: Infinity,
repeatType: "loop",
},
});
};
useEffect(() => {
void handleDragEnd();
}, []);
const modeIndex = ["skeleton", "normal", "glass"].indexOf(mode);
const dimensionModeIndex = dimensionModes.findIndex(
(entry) => entry.mode === dimensionMode
);
return (
<div className="relative flex items-center justify-center">
<motion.div
style={{
height:
size *
(dimensionModeIndex === 0
? 2
: dimensionModeIndex === 1
? 3
: 4),
width:
size *
(dimensionModeIndex === 0
? 2
: dimensionModeIndex === 1
? 3
: 4),
perspective: "10000px",
transformStyle: "preserve-3d",
rotateX: dragRotateX,
rotateY: dragRotateY,
cursor: "grab",
}}
animate={controls}
drag
dragConstraints={{ left: 0, right: 0, top: 0, bottom: 0 }}
dragElastic={0.1}
dragSnapToOrigin
onDrag={(_, info) => {
rotateY.set(rotateY.get() + info.delta.x * 0.5);
rotateX.set(rotateX.get() - info.delta.y * 0.5);
}}
onDragStart={() => controls.stop()}
onDragEnd={() => {
void handleDragEnd();
}}
className="relative flex flex-wrap items-center justify-center"
>
{faceColors.map((config, index) => (
<Face
key={`face-${index}`}
config={config}
mode={modeIndex}
dimensionMode={dimensionModeIndex}
/>
))}
</motion.div>
</div>
);
}
export default RubikCube;