Documentation Index
Fetch the complete documentation index at: https://dadd.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
A controller is a React component that calls useBeforePhysicsStep to write data.ctrl each frame and renders null.
The useIkController() hook follows this same pattern. You can use it, swap in your own IK solver, or write your own controller from scratch.
Pattern: Simple Keyboard Bindings
For robots where arm control comes from <IkGizmo />, the controller only adds extra bindings (gripper, etc.):
import { useKeyboardTeleop } from "mujoco-react";
export function FrankaController() {
useKeyboardTeleop({
bindings: {
v: { actuator: "gripper", toggle: [0, 255] },
},
});
return null;
}
Drop it into your scene as a child of <MujocoCanvas>:
function FrankaScene() {
const ik = useIkController({ siteName: "tcp", numJoints: 7 });
return (
<>
{ik && <IkGizmo controller={ik} />}
<FrankaController />
</>
);
}
<MujocoCanvas config={frankaConfig}>
<FrankaScene />
</MujocoCanvas>
Pattern: Custom Physics-Step Control
For more complex control (IK solvers, velocity control, state machines), use useBeforePhysicsStep to write directly to data.ctrl each frame.
Keyboard State
Read keyboard input via window event listeners and a ref:
import { useEffect, useRef } from "react";
import { useBeforePhysicsStep } from "mujoco-react";
function MyController() {
const keys = useRef<Record<string, boolean>>({});
useEffect(() => {
const down = (e: KeyboardEvent) => { keys.current[e.code] = true; };
const up = (e: KeyboardEvent) => { keys.current[e.code] = false; };
window.addEventListener("keydown", down);
window.addEventListener("keyup", up);
return () => {
window.removeEventListener("keydown", down);
window.removeEventListener("keyup", up);
};
}, []);
useBeforePhysicsStep((_model, data) => {
const k = keys.current;
if (k["KeyW"]) data.ctrl[0] += 0.01;
if (k["KeyS"]) data.ctrl[0] -= 0.01;
});
return null;
}
Config-Driven Arm Controller
A generic hook that accepts a static config object makes it easy to support multiple robots. Each robot is a different config:
interface ArmConfig {
indices: number[]; // Actuator indices for this arm
keys: string[]; // Key codes for movement
initialJoints?: number[]; // Starting joint positions
}
interface ArmControllerConfig {
numActuators: number;
arms: ArmConfig[];
base?: { ... }; // Mobile base drive
head?: { ... }; // Pan/tilt head
}
The hook reads keyboard state and writes to the correct actuator indices each frame:
function useArmController(config: ArmControllerConfig) {
const keys = useRef<Record<string, boolean>>({});
// ... keyboard listeners ...
useBeforePhysicsStep((_model, data) => {
for (const arm of config.arms) {
// Read keys, solve IK, write to data.ctrl[arm.indices[i]]
}
});
}
Then each robot controller is just a config:
const SO101_CONFIG: ArmControllerConfig = {
numActuators: 6,
arms: [{
indices: [0, 1, 2, 3, 4, 5],
keys: ["KeyD", "KeyA", "KeyW", "KeyS", "KeyQ", "KeyE",
"KeyR", "KeyF", "KeyZ", "KeyC", "KeyV"],
initialJoints: [0.0158, 2.052, 2.1307, -0.0845, 1.5857, -0.3745],
}],
};
export function SO101Controller() {
useArmController(SO101_CONFIG);
return null;
}
Custom IK Solvers
Three options for IK:
1. Use the built-in solver
The default useIkController() uses Damped Least-Squares:
const ik = useIkController({ siteName: "tcp", numJoints: 7 });
return ik ? <IkGizmo controller={ik} /> : null;
2. Plug in your own solver
Pass ikSolveFn to replace the built-in solver while keeping the gizmo, reset handling, and context:
import type { IKSolveFn } from "mujoco-react";
const myIK: IKSolveFn = (pos, quat, currentQ) => {
return myAnalyticalSolver(pos, currentQ); // return joint angles or null
};
const ik = useIkController({ siteName: "tcp", numJoints: 7, ikSolveFn: myIK });
return ik ? <IkGizmo controller={ik} /> : null;
3. Skip useIkController entirely
Solve IK yourself inside useBeforePhysicsStep with full access to the model and data:
function MyIKController({ targetRef }) {
useBeforePhysicsStep((model, data) => {
const target = targetRef.current;
if (!target) return;
const joints = myCustomIKSolve(model, data, target);
if (joints) {
for (let i = 0; i < joints.length; i++) data.ctrl[i] = joints[i];
}
});
return null;
}
This gives you full access to model/data for whatever solver you want.
Pattern: Reusable Plugins with createControllerHook
For reusable controllers with typed config and default merging, use the createControllerHook factory. It stabilizes config references (so inline objects don’t cause re-renders), merges defaults, and supports disabling via null.
import { createControllerHook, useBeforePhysicsStep } from "mujoco-react";
interface MyConfig {
gain: number;
actuatorIndex: number;
frequency?: number;
}
export const useMyController = createControllerHook<MyConfig, { amplitude: number }>(
{ name: "useMyController", defaultConfig: { gain: 1.0, frequency: 1.0 } },
(config) => {
const amplitudeRef = useRef(0);
useBeforePhysicsStep((_model, data) => {
if (!config) return;
const freq = config.frequency ?? 1.0;
amplitudeRef.current = config.gain * Math.sin(data.time * freq);
data.ctrl[config.actuatorIndex] = amplitudeRef.current;
});
if (!config) return null;
return { amplitude: amplitudeRef.current };
},
);
// const result = useMyController({ gain: 2.0, actuatorIndex: 0 });
// const disabled = useMyController(null); // returns null, no-ops
createControllerHook API
function createControllerHook<TConfig, TValue>(
options: { name: string; defaultConfig?: Partial<TConfig> },
useImpl: (config: TConfig | null) => TValue | null,
): (config: TConfig | null) => TValue | null;
Pass null to disable the controller without breaking the rules of hooks — useImpl is always called, it just receives null and should no-op.
Pattern: Reusable Plugins with createController
The createController factory is the component equivalent — same config stabilization and default merging, but returns a component that can render children:
import { createController, useBeforePhysicsStep } from "mujoco-react";
interface MyConfig {
gain: number;
actuatorIndex: number;
frequency?: number;
}
function MyControllerImpl({ config, children }: { config: MyConfig; children?: React.ReactNode }) {
useBeforePhysicsStep((_model, data) => {
const freq = config.frequency ?? 1.0;
data.ctrl[config.actuatorIndex] = config.gain * Math.sin(data.time * freq);
});
return <>{children}</>;
}
export const MyController = createController<MyConfig>(
{ name: "MyController", defaultConfig: { gain: 1.0, frequency: 1.0 } },
MyControllerImpl,
);
// <MyController config={{ gain: 2.0, actuatorIndex: 0 }}>
// <Debug showJoints />
// </MyController>
createController API
function createController<TConfig>(
options: { name: string; defaultConfig?: Partial<TConfig> },
Impl: React.FC<{ config: TConfig; children?: React.ReactNode }>,
): ControllerComponent<TConfig>;
The returned component accepts config (merged with defaults) and optional children. It also exposes static metadata: MyController.controllerName and MyController.defaultConfig.
Providing Context to Children
Controllers can provide state to descendants via React context:
const MyContext = createContext<MyContextValue | null>(null);
function MyControllerImpl({ config, children }) {
const value = useMemo(() => ({ /* state + methods */ }), []);
return <MyContext.Provider value={value}>{children}</MyContext.Provider>;
}
Listening for Resets
Register a callback to reset your controller state when the simulation resets:
function MyControllerImpl({ config, children }) {
const { resetCallbacks } = useMujoco();
useEffect(() => {
const cb = () => { /* reset your state */ };
resetCallbacks.current.add(cb);
return () => { resetCallbacks.current.delete(cb); };
}, [resetCallbacks]);
return <>{children}</>;
}
The library’s useIkController() hook demonstrates all these patterns: reset handling, useBeforePhysicsStep for solving, and useFrame for gizmo animation.
Coexisting with IK Gizmo
When a robot supports both gizmo drag and keyboard control, the controller needs to:
- Accept
ik as a prop (the IkContextValue from useIkController())
- Sync state on transition: when the user switches from gizmo to keyboard, read
data.ctrl to avoid a position jump
- Disable IK via
ik.setIkEnabled(false) when taking over
import { useBeforePhysicsStep } from "mujoco-react";
import type { IkContextValue } from "mujoco-react";
function MyArmController({ ik }: { ik?: IkContextValue | null }) {
useBeforePhysicsStep((_model, data) => {
const anyKeyPressed = /* check keyboard state */;
if (anyKeyPressed && ik?.ikEnabledRef.current) {
// Sync from current gizmo position
for (let i = 0; i < arm.indices.length; i++) {
targetJoints[i] = data.ctrl[arm.indices[i]];
}
ik.setIkEnabled(false);
}
if (!ik?.ikEnabledRef.current) {
// Keyboard is in control, solve IK and write ctrl
for (let i = 0; i < arm.indices.length; i++) {
data.ctrl[arm.indices[i]] = targetJoints[i];
}
}
});
}
Pass the ik value from useIkController() to the controller as a prop.
The gizmo re-enables IK automatically when dragged.
Composing Controllers in Your Scene
Controllers are React children. Swap them based on state:
function SceneChildren({ robotKey, ikConfig, showGizmo }) {
const ik = useIkController(ikConfig);
return (
<>
{ik && showGizmo && <IkGizmo controller={ik} />}
<DragInteraction />
{robotKey === "franka" && <FrankaController />}
{robotKey === "so101" && <SO101Controller ik={ik} />}
{robotKey === "xlerobot" && <XLeRobotController ik={ik} />}
</>
);
}
<MujocoCanvas config={entry.config}>
<SceneChildren robotKey={robotKey} ikConfig={ikConfig} showGizmo={showGizmo} />
</MujocoCanvas>
- Use
for loops instead of .forEach / .map in useBeforePhysicsStep (it runs every physics tick)
- Store keyboard state in a
useRef, not useState (avoids re-renders at 60fps)
- Cache actuator IDs once (not every frame) using
findActuatorByName
- Keep the callback closure stable; avoid creating new functions each render