How to Use Matter.js in Next.js Without SSR Errors

Integrating Matter.js into Next.js applications often results in server-side rendering (SSR) errors like window is not defined because the physics engine relies directly on browser-only APIs. This guide explains why these SSR issues occur and details how to solve them using Next.js dynamic imports, client-side lifecycle hooks, and proper physics engine cleanup routines.

Why Matter.js Fails During SSR

Next.js executes code on the Node.js server before delivering the rendered HTML to the client. Matter.js immediately evaluates globals such as window, document, and browser-based rendering APIs (such as HTMLCanvasElement and requestAnimationFrame). Because these do not exist in Node.js, standard static imports (import Matter from 'matter-js') cause build or runtime crashes during server execution.

Solution 1: Dynamic Import with SSR Disabled

The most robust way to prevent Matter.js from executing on the server is using next/dynamic with { ssr: false }. This ensures the component containing the physics logic is only downloaded and mounted in the browser.

Create an isolated physics component:

// components/PhysicsCanvas.tsx
'use client';

import { useEffect, useRef } from 'react';
import Matter from 'matter-js';

export default function PhysicsCanvas() {
  const sceneRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!sceneRef.current) return;

    const { Engine, Render, Runner, Bodies, Composite } = Matter;

    const engine = Engine.create();
    const render = Render.create({
      element: sceneRef.current,
      engine: engine,
      options: {
        width: 800,
        height: 600,
        wireframes: false,
      },
    });

    const box = Bodies.rectangle(400, 200, 80, 80);
    const ground = Bodies.rectangle(400, 590, 810, 60, { isStatic: true });

    Composite.add(engine.world, [box, ground]);

    Render.run(render);
    const runner = Runner.create();
    Runner.run(runner, engine);

    return () => {
      Render.stop(render);
      Runner.stop(runner);
      Composite.clear(engine.world, false);
      Engine.clear(engine);
      render.canvas.remove();
    };
  }, []);

  return <div ref={sceneRef} />;
}

Import it dynamically in your page:

// app/page.tsx or pages/index.tsx
import dynamic from 'next/dynamic';

const PhysicsCanvas = dynamic(() => import('@/components/PhysicsCanvas'), {
  ssr: false,
});

export default function Page() {
  return (
    <main>
      <h1>Matter.js Next.js Simulation</h1>
      <PhysicsCanvas />
    </main>
  );
}

Solution 2: Inline Dynamic Import Inside useEffect

If you prefer keeping your imports within a single component file without setting up dynamic wrappers, load Matter.js dynamically inside a useEffect hook. Since useEffect only runs on the client side, Node.js will never attempt to execute the module.

'use client';

import { useEffect, useRef } from 'react';

export default function PhysicsView() {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    let render: any;
    let runner: any;
    let engine: any;

    async function initMatter() {
      const Matter = await import('matter-js');
      const { Engine, Render, Runner, Bodies, Composite } = Matter.default || Matter;

      if (!containerRef.current) return;

      engine = Engine.create();
      render = Render.create({
        element: containerRef.current,
        engine: engine,
        options: { width: 600, height: 400 },
      });

      const circle = Bodies.circle(300, 50, 30);
      const floor = Bodies.rectangle(300, 390, 600, 20, { isStatic: true });

      Composite.add(engine.world, [circle, floor]);

      Render.run(render);
      runner = Runner.create();
      Runner.run(runner, engine);
    }

    initMatter();

    return () => {
      if (render) Render.stop(render);
      if (runner) Runner.stop(runner);
      if (engine) Engine.clear(engine);
    };
  }, []);

  return <div ref={containerRef} />;
}

Crucial Cleanup Considerations

When working with Next.js, React Fast Refresh and Strict Mode remount components frequently during development. If Matter.js instances are not manually dismantled in the useEffect cleanup return function:

  1. Multiple <canvas> elements will stack on top of each other.
  2. Background loops (Runner.run) will continue executing, leading to memory leaks and erratic performance.
  3. Call Render.stop(), Runner.stop(), and remove the created canvas from the DOM to maintain consistent client performance.