GodUIGodUI
129Follow on X

Anatomy of Gravity

How a matter-js world lives inside a React tree, why the engine is created at render instead of in an effect, and how a rAF loop turns physics bodies into moving DOM.

01/04One world, thick walls, a few bodies
World
relative overflow-hidden container, sized by CSS
Walls
static 200px-thick rectangles just past each edge
Body
one MatterBody = one rectangle or circle body
tsx
const t = 200; // thick walls keep fast bodies containedconst walls = [Matter.Bodies.rectangle(width / 2, height + t / 2, width + t * 2, t, opts),Matter.Bodies.rectangle(-t / 2, height / 2, t, height + t * 2, opts),Matter.Bodies.rectangle(width + t / 2, height / 2, t, height + t * 2, opts),];if (addTopWall) {walls.push(Matter.Bodies.rectangle(width / 2, -t / 2, width + t * 2, t, opts));}
01

One world, thick walls, a few bodies

Gravity is an invisible physics world running underneath ordinary DOM elements. matter-js never draws anything; every frame, a sync loop copies each body's position straight into style.transform on the element you see.

The canvas is a plain relative overflow-hidden div. On mount, Gravity adds static rectangle walls just past each edge (200px thick, so a body moving fast can't tunnel through in a single frame), and every MatterBody child becomes one more rectangle or circle body in that same world.

A ResizeObserver rebuilds the walls whenever the canvas changes size, so they always sit just outside whatever the container currently measures.

The engine is created at render, not in an effect:

tsx
 
const [engine] = React.useState(() => Matter.Engine.create());

This is the one non-obvious line in the file. React fires child effects first, so a child MatterBody's effect calls ctx.register(...) before the parent Gravity's own effect runs. If the engine were created inside Gravity's effect, every body registered by a child would already have tried to add itself to a world that doesn't exist yet. Creating it during render (via lazy useState) guarantees the engine and its world exist before any child can register a body against it.

x/y accept a % string, a px string, or a number, so a body can be placed relative to a container whose size isn't known at author time:

tsx
 
function resolve(value: number | string, size: number): number {
  if (typeof value === "number") return value;
  if (value.endsWith("%")) return (parseFloat(value) / 100) * size;
  return parseFloat(value);
}

This runs once, at registration. After that, the body is a free physics object and its position is whatever the simulation computes, not the original percentage.

02/04Position and angle become style.transform

transform: translate(x - w/2, y - h/2) rotate(angle)

Body (physics)
Matter.Body position + angle, never rendered
DOM plate
style.transform, rewritten every rAF
tsx
const sync = () => {for (const { element, body } of bodiesRef.current.values()) {  const w = element.offsetWidth;  const h = element.offsetHeight;  element.style.transform = `translate(${body.position.x - w / 2}px, ${    body.position.y - h / 2  }px) rotate(${body.angle}rad)`;}syncFrame = requestAnimationFrame(sync);};
02

Position and angle become style.transform

Matter.js has no idea a DOM exists. A sync function, scheduled with requestAnimationFrame, reads every registered body's position and angle each frame and writes them into the matching element's transform. Nothing else moves the element.

Subtracting half the element's own width and height re-centers the translation, because Matter tracks a body's center while transform moves from the element's top-left origin. translate/rotate are both compositor-only: the browser moves an already-painted layer without re-running layout, which keeps dozens of simultaneously falling bodies smooth.

A single shared Matter.MouseConstraint handles every pointer interaction in the world. Each MatterBody carries its own isDraggable flag, checked the moment a drag starts:

tsx
 
Matter.Events.on(mouseConstraint, "startdrag", (e) => {
  const dragged = [...bodiesRef.current.values()].find(
    (b) => b.body === (e as unknown as { body: Matter.Body }).body,
  );
  if (dragged && !dragged.isDraggable) {
    mouseConstraint.constraint.bodyB = null;
  }
});

If the body that was grabbed isn't draggable, the constraint's target is cleared on the same tick. The pointer never attaches, so the body keeps falling under gravity instead of following the cursor.

03/04Cheap when idle
in view
off-screen / hidden tab

Runner.stop(runner) · cancelAnimationFrame(syncFrame)

Running
intersecting and tab visible: runner + sync loop active
Paused
off-screen or tab hidden: both loops stopped
tsx
const io = new IntersectionObserver(([entry]) => {  visible = entry.isIntersecting;  if (visible && !document.hidden) resume();  else pause();},{ threshold: 0 },);document.addEventListener("visibilitychange", () => {if (document.hidden) pause();else if (visible) resume();});
03

Cheap when idle

A physics world sitting off screen, or behind a hidden tab, has no business burning frames. An IntersectionObserver on the canvas and a visibilitychange listener on the document both gate the same two functions: resume() starts the Matter Runner and reschedules the rAF sync loop; pause() stops both.

Either condition failing is enough to pause. Visible-but-hidden-tab and intersecting-but-off-screen both stop the simulation, and either one becoming true again resumes it where the bodies were left.

04/04Result
design
motion
physics
react
04

The result

An invisible matter-js world mirrored onto the DOM with one transform write per frame, paused while the canvas is off screen or the tab is hidden.

Reduced motion

Under prefers-reduced-motion, Gravity never creates an engine and MatterBody never registers. Each body lays itself out statically at its authored x/y/angle using ordinary absolute positioning. No physics runs and no walls exist.

tsx
 
const staticPlacement =
  !ctx || ctx.reduced
    ? {
        left: typeof x === "number" ? `${x}px` : x,
        top: typeof y === "number" ? `${y}px` : y,
        transform: `translate(-50%, -50%) rotate(${angle}deg)`,
      }
    : undefined;

Motion Score

GravityCC: Paint-triggering
Cphysics framematter-js simulation each frame
Each property is graded by how the browser runs it, from S (composited off the main thread) down to F (layout thrashing); the component takes the worst. MotionScore methodology →