Robot stage
The room the 3D robots stand in: lights, a contact shadow, an optional grid floor, and orbit controls.
floor
rotate
Install
pnpm dlx shadcn@latest add https://robocn.dev/r/robot-stage.jsonUsage
<RobotStage camera={[3.6, 2.6, 4.6]} floor="grid" autoRotate>
<RobotArm3D behavior="sweep" />
</RobotStage>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| camera | [number, number, number] | [3.6, 2.6, 4.6] | Camera position. The default frames a 2.5-unit machine. |
| fov | number | 40 | Field of view. |
| controls | boolean | true | Drag to orbit. Off makes the stage a static illustration. |
| autoRotate | boolean | false | Turn slowly. |
| floor | "grid" | "shadow" | "none" | "shadow" | Floor treatment under the machine. |
| canvasProps | Partial<CanvasProps> | — | Anything else the react-three-fiber canvas takes. |
Source
src/components/ui/robot-stage.tsx
"use client"
/**
* robot-stage — the room the 3D robots stand in.
*
* A configured react-three-fiber canvas: key and fill lights, a contact
* shadow, an optional grid floor and orbit controls. Drop any robocn 3D
* component inside it and it is lit and framed without further setup.
*/
import * as React from "react"
import { ContactShadows, Grid, OrbitControls } from "@react-three/drei"
import { Canvas, type CanvasProps } from "@react-three/fiber"
import { resolveCssColor } from "@/lib/robocn/color"
import { resolveRobotPalette, type RobotPaletteProps } from "@/lib/robocn/style"
import { cn } from "@/lib/utils"
export interface RobotStageProps
extends Omit<React.ComponentProps<"div">, "children">,
RobotPaletteProps {
children?: React.ReactNode
/** Camera position in world units. The default frames a 2.5-unit machine. */
camera?: [number, number, number]
fov?: number
/** Drag to orbit. Off makes the stage a static illustration. */
controls?: boolean
autoRotate?: boolean
/** Floor treatment under the machine. */
floor?: "grid" | "shadow" | "none"
/** Extra three.js props, e.g. `{ dpr: 2 }`. */
canvasProps?: Partial<CanvasProps>
}
function RobotStage({
children,
camera = [3.6, 2.6, 4.6],
fov = 40,
controls = true,
autoRotate = false,
floor = "shadow",
canvasProps,
className,
color,
accent,
metal,
dark,
glow,
grid,
palette: paletteOverride,
...props
}: RobotStageProps) {
const palette = resolveRobotPalette({
color,
accent,
metal,
dark,
glow,
grid,
palette: paletteOverride,
})
// three.js parses neither CSS variables nor oklch, and a floor drawn in
// unparsed colour comes out white.
const gridColor = useThreeColor(palette.grid, "#8d94a1")
return (
<div className={cn("relative h-80 w-full", className)} {...props}>
<Canvas
shadows
camera={{ position: camera, fov }}
gl={{ antialias: true, alpha: true }}
{...canvasProps}
>
<hemisphereLight intensity={0.55} groundColor="#20242b" />
<directionalLight
position={[4, 6, 3]}
intensity={2.1}
castShadow
shadow-mapSize={[1024, 1024]}
shadow-normalBias={0.02}
/>
<directionalLight position={[-5, 2, -3]} intensity={0.5} />
{floor === "grid" ? (
<Grid
args={[20, 20]}
cellSize={0.5}
cellThickness={0.6}
sectionSize={2}
sectionThickness={1}
fadeDistance={16}
infiniteGrid
sectionColor={gridColor}
cellColor={gridColor}
position={[0, 0.001, 0]}
/>
) : null}
{floor !== "none" ? (
<ContactShadows
position={[0, 0.002, 0]}
opacity={0.45}
scale={12}
blur={2.2}
far={4}
/>
) : null}
{children}
{controls ? (
<OrbitControls
makeDefault
autoRotate={autoRotate}
autoRotateSpeed={0.8}
enablePan={false}
minPolarAngle={0.2}
maxPolarAngle={Math.PI / 2.05}
minDistance={2.4}
maxDistance={14}
/>
) : null}
</Canvas>
</div>
)
}
/**
* One palette role as a colour three.js can use. Re-read when the document's
* theme changes, so a dark-mode toggle retints the floor without a remount.
*/
function useThreeColor(value: string, fallback: string) {
const [resolved, setResolved] = React.useState(fallback)
React.useEffect(() => {
const read = () => setResolved(resolveCssColor(value, fallback))
read()
const observer = new MutationObserver(read)
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class", "style", "data-theme"],
})
return () => observer.disconnect()
}, [value, fallback])
return resolved
}
export { RobotStage }