Matter.js Reference
Matter.js is a 2D rigid-body physics engine for gravity, collisions, rotation, friction, constraints, sensors, and interactive simulations.
Use Matter.js alone when objects need rigid-body physics and its simple renderer is sufficient. Use p5.js alone for creative drawing, particles, procedural animation, and visualizations without rigid-body collisions. Combine them only when Matter.js should own the physics and p5 should provide custom rendering.
Imports and React setup
import Matter from "matter-js";
Create the engine and renderer inside useEffect. Keep mutable physics objects out of React state so high-frequency simulation changes do not trigger component renders.
import * as React from "react";
import Matter from "matter-js";
function PhysicsCanvas() {
const containerRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
const container = containerRef.current;
if (!container) return;
const width = container.clientWidth;
const height = 420;
const engine = Matter.Engine.create();
const ground = Matter.Bodies.rectangle(
width / 2,
height - 12,
width,
24,
{ isStatic: true },
);
const ball = Matter.Bodies.circle(width / 2, 60, 24, {
restitution: 0.75,
});
Matter.Composite.add(engine.world, [ground, ball]);
const render = Matter.Render.create({
element: container,
engine,
options: {
width,
height,
wireframes: false,
background: "transparent",
pixelRatio: window.devicePixelRatio,
},
});
const runner = Matter.Runner.create({ delta: 1000 / 60 });
Matter.Render.run(render);
Matter.Runner.run(runner, engine);
return () => {
Matter.Runner.stop(runner);
Matter.Render.stop(render);
Matter.Composite.clear(engine.world, false, true);
Matter.Engine.clear(engine);
render.canvas.remove();
};
}, []);
return <div ref={containerRef} className="w-full touch-none" />;
}
Matter.Render is suitable for prototypes and simple games. Use custom Canvas or p5 rendering for more polished visuals.
Bodies and materials
Create common shapes with Bodies and add them through Composite.
const box = Matter.Bodies.rectangle(120, 80, 64, 44, {
density: 0.002,
friction: 0.7,
restitution: 0.2,
});
const ball = Matter.Bodies.circle(240, 80, 24, {
restitution: 0.85,
});
const triangle = Matter.Bodies.polygon(340, 80, 3, 30);
const ramp = Matter.Bodies.trapezoid(220, 300, 220, 24, 0.35, {
isStatic: true,
});
Matter.Composite.add(engine.world, [box, ball, triangle, ramp]);
Useful body options include isStatic, isSensor, friction, frictionAir, restitution, density, angle, label, and collisionFilter.
Move bodies with Matter APIs rather than changing their properties directly:
Matter.Body.setPosition(box, { x: 180, y: 120 });
Matter.Body.setVelocity(ball, { x: 8, y: -5 });
Matter.Body.setAngle(box, Math.PI / 4);
Matter.Body.applyForce(ball, ball.position, { x: 0.02, y: -0.04 });
Collisions and sensors
Sensors report overlaps without pushing bodies apart. They work well for goals, checkpoints, triggers, and out-of-bounds zones.
const goal = Matter.Bodies.rectangle(320, 360, 120, 24, {
isStatic: true,
isSensor: true,
label: "goal",
});
const player = Matter.Bodies.circle(80, 80, 20, { label: "player" });
Matter.Composite.add(engine.world, [goal, player]);
const handleCollisionStart = (
event: Matter.IEventCollision<Matter.Engine>,
) => {
for (const pair of event.pairs) {
const labels = new Set([pair.bodyA.label, pair.bodyB.label]);
if (labels.has("goal") && labels.has("player")) {
// Report completion through a guarded callback or ref.
}
}
};
Matter.Events.on(engine, "collisionStart", handleCollisionStart);
// In cleanup:
Matter.Events.off(engine, "collisionStart", handleCollisionStart);
The collision events are collisionStart, collisionActive, and collisionEnd. Avoid updating React state on every active collision.
Constraints and assemblies
Constraints connect two bodies or connect a body to a world-space point. Use length: 0 and high stiffness for a pivot, lower stiffness for a spring, and linked bodies for a rope.
const plank = Matter.Bodies.rectangle(220, 180, 180, 18);
const pivot = Matter.Constraint.create({
pointA: { x: 220, y: 180 },
bodyB: plank,
pointB: { x: 0, y: 0 },
length: 0,
stiffness: 0.9,
});
Matter.Composite.add(engine.world, [plank, pivot]);
Compound bodies made with Body.create({ parts }) are one rigid object. Their parts cannot rotate relative to each other. Wheels, axles, hinges, ragdolls, and articulated machines need separate bodies connected by constraints.
const carGroup = Matter.Body.nextGroup(true);
const chassis = Matter.Bodies.rectangle(200, 220, 120, 28, {
collisionFilter: { group: carGroup },
});
const wheel = Matter.Bodies.circle(160, 245, 22, {
collisionFilter: { group: carGroup },
});
const axle = Matter.Constraint.create({
bodyA: chassis,
pointA: { x: -40, y: 14 },
bodyB: wheel,
length: 0,
stiffness: 0.9,
});
Matter.Composite.add(engine.world, [chassis, wheel, axle]);
Mouse and queries
Bind mouse and touch dragging to the actual canvas:
const mouse = Matter.Mouse.create(render.canvas);
const mouseConstraint = Matter.MouseConstraint.create(engine, {
mouse,
constraint: {
stiffness: 0.2,
damping: 0.1,
render: { visible: false },
},
});
render.canvas.style.touchAction = "none";
render.mouse = mouse;
Matter.Composite.add(engine.world, mouseConstraint);
// In cleanup:
detachMatterMouse(mouse);
Mouse.clearSourceEvents only clears captured event data; it does not detach the DOM listeners installed by Mouse.create. Define and call this helper during cleanup:
type MatterMouse = ReturnType<typeof Matter.Mouse.create>;
function detachMatterMouse(mouse: MatterMouse) {
const element = mouse.element;
const handlers = mouse as MatterMouse & {
mousemove: EventListener;
mousedown: EventListener;
mouseup: EventListener;
mousewheel: EventListener;
};
element.removeEventListener("mousemove", handlers.mousemove);
element.removeEventListener("mousedown", handlers.mousedown);
element.removeEventListener("mouseup", handlers.mouseup);
element.removeEventListener("wheel", handlers.mousewheel);
element.removeEventListener("touchmove", handlers.mousemove);
element.removeEventListener("touchstart", handlers.mousedown);
element.removeEventListener("touchend", handlers.mouseup);
Matter.Mouse.clearSourceEvents(mouse);
}
Queries inspect bodies without changing the simulation:
const bodies = Matter.Composite.allBodies(engine.world);
const underPointer = Matter.Query.point(bodies, { x, y });
const inArea = Matter.Query.region(bodies, {
min: { x: 0, y: 0 },
max: { x: 200, y: 200 },
});
const rayHits = Matter.Query.ray(
bodies,
{ x: 20, y: 40 },
{ x: 380, y: 40 },
);
Query.ray reports collisions but does not return exact intersection points.
Query.region matches axis-aligned body bounds that overlap the region; it does not require full containment.
Custom rendering and timing
When p5 or another custom canvas loop owns scheduling, do not also call Runner.run. Advance Matter with a fixed-timestep accumulator so physics speed does not depend on display frame rate.
const STEP = 1000 / 60;
let accumulator = 0;
p.draw = () => {
accumulator += Math.min(p.deltaTime, 100);
while (accumulator >= STEP) {
Matter.Engine.update(engine, STEP);
accumulator -= STEP;
}
p.background("#0f172a");
for (const body of Matter.Composite.allBodies(engine.world)) {
const parts = body.parts.length > 1 ? body.parts.slice(1) : [body];
for (const part of parts) {
if (part.circleRadius) {
p.circle(part.position.x, part.position.y, part.circleRadius * 2);
continue;
}
// Matter vertices are already in world space.
p.beginShape();
for (const vertex of part.vertices) {
p.vertex(vertex.x, vertex.y);
}
p.endShape(p.CLOSE);
}
}
};
Changing canvas size does not move physical walls or rescale bodies. Resize the canvas, then remove and rebuild boundary bodies for the new dimensions.
Cleanup and limits
- Stop each
RunnerandRenderloop. - Detach the DOM listeners installed by
Mouse.create, then clear its captured source events. - Remove custom event listeners.
- Clear the world and engine.
- Remove the generated canvas.
- Keep engines, bodies, and frame-by-frame values in refs or effect-local variables; use React state only for coarse UI such as score and completion.
- Matter.js simulates rigid bodies, not fluids.
- Soft bodies and ropes are approximations made from particles and constraints.
- Very fast or thin bodies can tunnel because general continuous collision detection is not provided.
- Core Matter.js handles convex polygons. Automatic concave decomposition in
Bodies.fromVerticesrequires the unavailablepoly-decomppackage; compose complex shapes from convex parts. - Plugins such as
matter-attractorsandmatter-wrapare not available. - Use
Runner.create()beforeRunner.run(runner, engine)in Matter.js 0.20. - Coordinates use screen space, so positive y points down and angles are in radians.