Browse documentation

Robot quadruped

A four-legged robot with solved hip, knee, and foot positions. Scrub a walking or trotting cycle, change the stance, and inspect which feet touch the ground.

QUAD / 11
variant
gait
phase
65%
height
50%
stride
60%
lift
50%

Scrub the cycle to inspect footfall timing. Coloured marks identify feet on the ground.

Install

pnpm dlx shadcn@latest add https://robocn.dev/r/robot-quadruped.json

Notes

  • Phase is controlled and updates immediately; there is no internal animation loop. Connect a timeline or telemetry to animate it, and honor reduced-motion preferences in that driving code.
  • All four legs solve two-link inverse kinematics in parallel planes. The illustration preserves link lengths but does not simulate balance, forces, or terrain.

Usage

import { RobotQuadruped } from "@/components/ui/robot-quadruped"

<RobotQuadruped gait="trot" phase={0.65} height={0.5} stride={0.8} showContacts />

Props

PropTypeDefaultDescription
gait"stand" | "walk" | "trot""stand"Standing pose, staggered single-foot swings, or diagonal-pair swings.
phasenumber0Controlled cycle fraction. Wraps in both directions; ignored when standing. Non-finite values use zero.
heightnumber0.5Normalized stance height, clamped to 0–1 (36–54 world units at the hips). Non-finite values use 0.5.
stridenumber0.6Normalized fore/aft foot travel, clamped to 0–1. Zero steps in place. Non-finite values use 0.6.
liftnumber0.5Normalized foot clearance, clamped to 0–1. Zero slides feet along the ground. Non-finite values use 0.5.
showGroundbooleantrueGround reference plane; blueprint adds its grid.
showContactsbooleanfalseMark solved feet that touch the ground plane.
labelstringCaption underneath the robot.
variant"solid" | "outline" | "blueprint" | "wire""solid"How the machine is painted. Geometry never changes between variants.
size"xs" | "sm" | "md" | "lg" | "xl" | number"md"Rendered width in pixels, or a step on the scale.
colorstringvar(--robot-shell)Body panels — the colour the machine reads as.
accentstringvar(--robot-accent)Status colour: tip light, live tool, readouts.
metalstringvar(--robot-metal)Bare machined parts: collars, bolts, tool bodies.
darkstringvar(--robot-dark)Cast joints, base, shadow side.
palettePartial<RobotPalette>Override any subset of roles at once, including glow and grid.

Source

src/components/ui/robot-quadruped.tsx
"use client"

import type * as React from "react"
import { solveQuadruped, type QuadrupedLeg, type QuadrupedOptions } from "@/lib/robocn/quadruped"
import { capsulePath, px, resolveRobotPalette, resolveRobotSize, robotSurface, type RobotPaletteProps, type RobotSize, type RobotVariant } from "@/lib/robocn/style"
import { cn } from "@/lib/utils"

export interface RobotQuadrupedProps extends Omit<React.ComponentProps<"svg">, "color" | "height">, RobotPaletteProps, QuadrupedOptions {
  size?: RobotSize | number
  variant?: RobotVariant
  showGround?: boolean
  showContacts?: boolean
  label?: string
}

function RobotQuadruped({
  gait = "stand", phase = 0, height = 0.5, stride = 0.6, lift = 0.5,
  size = "md", variant = "solid", showGround = true, showContacts = false, label,
  color, accent, metal, dark, glow, grid, palette: paletteOverride, className, style, ...props
}: RobotQuadrupedProps) {
  const pose = solveQuadruped({ gait, phase, height, stride, lift })
  const palette = resolveRobotPalette({ color, accent, metal, dark, glow, grid, palette: paletteOverride })
  const width = resolveRobotSize(size)
  const shell = robotSurface("shell", variant, palette)
  const machined = robotSurface("metal", variant, palette)
  const cast = robotSurface("dark", variant, palette)
  const bodyY = 120 - pose.height

  function legDrawing(leg: QuadrupedLeg) {
    const far = leg.side === "right"
    return (
      <g key={leg.id} data-leg={leg.id} transform={`translate(${far ? 106 : 91} ${far ? 111 : 122}) scale(1 -1)`} opacity={far ? 0.55 : 1}>
        <path d={capsulePath(leg.hip, leg.knee, 3.8)} {...shell} />
        <path d={capsulePath(leg.knee, leg.foot, 2.6)} {...machined} />
        {[leg.hip, leg.knee].map((joint, i) => <g key={i}>
          <circle cx={px(joint.x)} cy={px(joint.y)} r={i === 0 ? 5.5 : 4} {...cast} />
          <circle cx={px(joint.x)} cy={px(joint.y)} r={1.6} fill={palette.metal} />
        </g>)}
        <rect x={px(leg.foot.x - 5)} y={px(leg.foot.y - 1.5)} width={10} height={3} rx={1.5} {...cast} />
        {showContacts && leg.contact && <ellipse data-contact cx={px(leg.foot.x)} cy={-3} rx={7} ry={1.3} fill={palette.accent} opacity={0.6} />}
      </g>
    )
  }

  return (
    <svg role="img" aria-label={`Quadruped robot, ${gait} pose`}
      viewBox="0 0 200 150" width={width} height={width * 0.75}
      className={cn("max-w-full select-none", className)} style={{ color: palette.foreground, ...style }} {...props}>
      {showGround && <g stroke={palette.grid} strokeWidth={0.5} opacity={0.5}>
        <path d="M 25 126 H 176 M 42 110 H 190 M 25 126 L 42 110 M 176 126 L 190 110" fill="none" />
        {variant === "blueprint" && [50, 75, 100, 125, 150].map(x => <path key={x} d={`M ${x} 126 l 16 -16`} strokeDasharray="1 2" />)}
      </g>}
      {pose.legs.filter(leg => leg.side === "right").map(legDrawing)}
      <g>
        <path d={`M 51 ${bodyY - 20} L 66 ${bodyY - 31} H 146 L 131 ${bodyY - 20} Z`} {...machined} />
        <path d={`M 131 ${bodyY - 20} L 146 ${bodyY - 31} V ${bodyY - 7} L 131 ${bodyY + 4} Z`} {...cast} />
        <rect x={51} y={bodyY - 20} width={80} height={24} rx={5} {...shell} />
        <rect x={73} y={bodyY - 15} width={35} height={14} rx={3} {...cast} />
        {[79, 85, 91, 97, 103].map(x => <line key={x} x1={x} y1={bodyY - 12} x2={x} y2={bodyY - 4} stroke={palette.metal} strokeWidth={1} />)}
        <path d={`M 67 ${bodyY - 23} l 8 -5 h 44 l -8 5 Z`} {...shell} />
        <rect x={134} y={bodyY - 22} width={23} height={13} rx={4} {...shell} />
        <rect x={145} y={bodyY - 19} width={11} height={7} rx={2} {...cast} />
        <circle cx={152} cy={bodyY - 15.5} r={1.5} fill={palette.accent} />
        <circle cx={62} cy={bodyY - 8} r={2} fill={palette.accent} />
      </g>
      {pose.legs.filter(leg => leg.side === "left").map(legDrawing)}
      {label && <text x={100} y={145} textAnchor="middle" fontFamily="ui-monospace, monospace" fontSize={4.5} fill={palette.foreground}>{label}</text>}
    </svg>
  )
}

export { RobotQuadruped }