Docs
Gravity

Gravity

A set of wrapper components for creating physics-based gravity animations with Matter.js.

fancy

components made with:

react
typescript
motion
tailwind
drei
matter-js

Setting up Matter.js for creating physics-based animations can be a bit tricky and cumbersome, especially with React. This component simplifies the process by wrapping your content with a physics world, and transforming your React components into Matter.js bodies.

Source code


First, install the following dependencies.

We use the matter-js library to create the physics simulation. The poly-decomp package is used to decompose bodies into a set of vertices, which is required for the svg body type. The svg-path-commander package is used to parse SVG paths and convert them into a set of vertices (since the built-in feature for this in matter-js is outdated).

npm install matter-js @types/matter-js poly-decomp svg-path-commander

Then, create two utility functions for the component.

One is for parsing SVG paths into a set of vertices:

import SVGPathCommander from 'svg-path-commander';

// Function to convert SVG path "d" to vertices
export function parsePathToVertices(path: string, sampleLength = 15) {
    // Convert path to absolute commands
    const commander = new SVGPathCommander(path);
    
    const points: { x: number, y: number }[] = [];
    let lastPoint: { x: number, y: number } | null = null;
    
    // Get total length of the path
    const totalLength = commander.getTotalLength();
    let length = 0;

    // Sample points along the path
    while (length < totalLength) {
        const point = commander.getPointAtLength(length);
        
        // Only add point if it's different from the last one
        if (!lastPoint || point.x !== lastPoint.x || point.y !== lastPoint.y) {
            points.push({ x: point.x, y: point.y });
            lastPoint = point;
        }
        
        length += sampleLength;
    }

    // Ensure we get the last point
    const finalPoint = commander.getPointAtLength(totalLength);
    if (lastPoint && (finalPoint.x !== lastPoint.x || finalPoint.y !== lastPoint.y)) {
        points.push({ x: finalPoint.x, y: finalPoint.y });
    }

    return points;
}

The other is for calculating the position of an element based on its container, and a posiiton value

export function calculatePosition(
  value: number | string | undefined,
  containerSize: number,
  elementSize: number
) {
  if (typeof value === "string" && value.endsWith("%")) {
    const percentage = parseFloat(value) / 100;
    return containerSize * percentage;
  }
  return typeof value === "number"
    ? value
    : elementSize - containerSize + elementSize / 2;
}

Then, create the component:

Usage


First, you need to wrap your scene / content with the Gravity component. Set the gravity direction vector in the gravity prop (default is {x: 0, y: 1}). Then, in order to transform your regular HTML elements into Matter bodies, you need to wrap them with the MatterBody component. You need to set each bodies x and y position, either as a percentage of your container size, or as a number. You do not need to set the width and height manually, everything else is taken care of by component :). High-level example:

<Gravity>
  <MatterBody x="50%" y="50%">
    <div>Hello world!</div>
  </MatterBody>
  <MatterBody x="10%" y="10%">
    <div>fancy!</div>
  </MatterBody>
</Gravity>

Understanding the component


Gravity component

At its core, the Gravity component creates and manages a Matter.js physics world. It handles:

  1. Physics Setup: Creates a canvas and initializes the Matter.js physics engine with:

    • A physics engine to calculate forces and collisions
    • A renderer to visualize the physics (when debug mode is enabled)
    • A runner to step the physics simulation forward
    • Mouse constraints to enable dragging of elements
  2. Animation Loop: Continuously updates the positions of your HTML elements to match their physics bodies in the Matter.js world. This creates the illusion that your DOM elements are actually affected by physics.

  3. Controls: Exposes three main methods:

    • start(): Begins the physics simulation
    • stop(): Pauses the physics simulation
    • reset(): Returns all elements to their starting positions
  4. Debug Mode: When enabled via the debug prop, shows the actual Matter.js physics bodies as overlays, which is super helpful for development.

MatterBody component

The MatterBody component transforms regular HTML elements into physics-enabled elements. Key features:

  • Positioning: Set initial positions with x and y props
<MatterBody x="50%" y="100px">
    <div>I'm physics-enabled!</div>
  </MatterBody>
  
  • Body Types: Choose between different physics shapes:

    • rectangle: Default, good for most elements
    • circle: Perfect for round elements
    • svg: For complex custom shapes
  • Physics Properties: Customize how elements behave with matterBodyOptions. The most commonly used options are:

<MatterBody 
    matterBodyOptions={{ 
      friction: 0.5,     // How slippery it is
      restitution: 0.7,  // How bouncy it is
      density: 0.001,    // How heavy it is
      isStatic: false,   // If true, the element won't move but can be collided with
      force: { x: 0, y: 0 } // Initial force applied to the body
      ... // More options
    }}
  >
    <div>I'm bouncy!</div>
  </MatterBody>
  

For a complete list of options, check out the Matter.js Body documentation. You can fine-tune everything from angular velocity to mass to create exactly the physics behavior you want.

Context

The components use React Context to communicate. When you wrap an element with MatterBody, it registers itself with the parent Gravity component. The registration process:

  1. Creates a Matter.js physics body matching your element's size and shape
  2. Adds the body to the physics world
  3. Sets up a sync system where the HTML element's position updates to match its physics body

Examples


Non-draggable bodies

By default, the MatterBody makes its element draggable. You can disable this behavior by setting the isDraggable prop to false. (Under the hood, we just add back the pointer-events to the elements, so they will be clickable, hover-able, etc, but the Matter body underneath will not receive any pointer events). This can be handy to create creative footers with clickable links for example:

CONTACT

LinkedIn
X (Twitter)
Instagram
GitHub
BlueSky

Different body types

With the bodyType prop, you can choose between different types of bodies. The available types are circle, rectangle, and svg.

In this example, we have a mixed of circle and rectangle bodies. Again, you do not need to define the sizes on the MatterBody component, you can define them on your component level, eg. adding w-12 h12 to your tailwind classes. Then, the component will calculate the size for the matter.js engine.

icons from lucide.dev

SVGs

The third bodyType option is svg, which allows you to create physics bodies from SVG elements. This is particularly useful for creating custom-shaped physics objects that match your SVG graphics.

Here's how it works:

  1. The component takes your SVG element and extracts the path data
  2. It converts the path into a series of vertices (points) that outline the shape (with a custom converter using the svg-path-commander package)
  3. These vertices are then converted into polygons by matter.js (with the help of the poly-decomp package).
  4. The resulting polygons are then used to create Matter.js bodies

As you can see in the demo above, SVG bodies can produce varying results. Simple shapes like the stars translate well, maintaining their shapes in the physics simulation. More complex shapes like the fancy text at the bottom (which is an SVG path, and not an HTML element) end up with rougher approximations.

This variance in quality stems from the challenging process of converting SVG paths to physics bodies. Therefore, there are a few caveats to keep in mind:

  1. SVG Requirements:

    • Keep them simple. The simpler the SVG, the better the decomposition, and the simulation.
    • It's only tested with single-path SVGs, and it probably won't work with nested paths.
    • Avoid shapes with holes or complex curves, or shapes that are seem to be too complex to decompose into polygons.
  2. Performance Impact:

    • Complex SVGs create more detailed physics bodies, which can slow down the simulation
    • More vertices mean more calculations
    • The initial path-to-vertices conversion can be slow.

If you're not getting the desired results, you have several options:

  1. Break down complex SVGs into simpler shapes
  2. Use basic physics bodies (rectangles/circles) with the SVG as a visual overlay
  3. Fine-tune the vertex sampling with the sampleLength prop

While the demo's fancy text on the bottom worked well by chance for me, you more than likely will need to experiment with different settings to get the desired results. Use the debug prop to visualize the physics bodies and their vertices, and adjust the sampleLength prop to control the accuracy of the conversion.

For more details on the decomposition process, refer to the poly-decomp documentation, the Matter.js documentation, and to the SVG path commander documentation.

Props


Gravity

PropTypeDefaultDescription
children*React.ReactNode-The content to be displayed and animated
debugbooleanfalseWhether to show the physics bodies and their vertices
gravity{ x: number; y: number }{ x: 0, y: 1 }The direction of gravity
resetOnResizebooleantrueWhether to reset the physics world when the window is resized
grabCursorbooleantrueWhether to show grab/grabbing cursor when interacting with bodies
addTopWallbooleantrueWhether to add a wall at the top of the canvas
autoStartbooleantrueWhether to automatically start the physics simulation
classNamestring-Additional CSS classes to apply to the container

MatterBody

PropTypeDefaultDescription
children*React.ReactNode-The content to be displayed and animated
matterBodyOptionsMatter.IBodyDefinition{ friction: 0.1, restitution: 0.1, density: 0.001, isStatic: false }Matter.js body configuration options
bodyType"rectangle" | "circle" | "svg""rectangle"The type of physics body to create
isDraggablebooleantrueWhether the body can be dragged with the mouse
sampleLengthnumber15The sampling distance for SVG path vertices
xnumber | string0Initial x position (can be percentage string)
ynumber | string0Initial y position (can be percentage string)
anglenumber0Initial rotation angle in degrees
classNamestring-Additional CSS classes to apply to the container