Nuxt 3 Matter.js Client-Only Plugin Setup

Matter.js is a popular 2D physics engine built for the web, but its reliance on browser APIs like window and document can trigger build or runtime errors during Nuxt 3 server-side rendering (SSR). This article explains how to safely wrap Matter.js into a client-only Nuxt 3 plugin, inject it globally, and consume it inside Vue components without encountering SSR conflicts.

1. Install Dependencies

First, install the matter-js package along with its TypeScript definitions:

npm install matter-js
npm install -D @types/matter-js

2. Create the Client-Only Plugin

Nuxt 3 uses a file-naming convention to determine execution context. Appending .client.ts to any file inside the plugins/ directory ensures Nuxt only loads and executes the code in the browser.

Create a file named plugins/matter.client.ts:

import Matter from 'matter-js'

export default defineNuxtPlugin(() => {
  return {
    provide: {
      matter: Matter
    }
  }
})

By returning an object with the provide key, Nuxt injects $matter into the application context, making the entire Matter.js namespace accessible in both script setups and templates.

3. Configure TypeScript Declarations (Optional)

To ensure TypeScript recognizes the $matter property on the Nuxt application instance, add a type declaration file (e.g., index.d.ts) in your root directory:

import type Matter from 'matter-js'

declare module '#app' {
  interface NuxtApp {
    $matter: typeof Matter
  }
}

declare module 'vue' {
  interface ComponentCustomProperties {
    $matter: typeof Matter
  }
}

export {}

4. Use Matter.js in a Component

Because canvas rendering and DOM measurements require the browser, initialize Matter.js modules inside the onMounted lifecycle hook. Always clean up runners and renderers in onBeforeUnmount to avoid memory leaks.

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'

const scene = ref<HTMLDivElement | null>(null)
const { $matter } = useNuxtApp()

let render: InstanceType<typeof $matter.Render>
let runner: InstanceType<typeof $matter.Runner>

onMounted(() => {
  if (!scene.value) return

  const { Engine, Render, Runner, Bodies, Composite } = $matter

  // Create engine
  const engine = Engine.create()

  // Create renderer
  render = Render.create({
    element: scene.value,
    engine: engine,
    options: {
      width: 800,
      height: 600,
      wireframes: false
    }
  })

  Render.run(render)

  // Create runner
  runner = Runner.create()
  Runner.run(runner, engine)

  // Add bodies
  const box = Bodies.rectangle(400, 200, 80, 80, { restitution: 0.8 })
  const ground = Bodies.rectangle(400, 590, 810, 60, { isStatic: true })

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

onBeforeUnmount(() => {
  if (render) {
    $matter.Render.stop(render)
    render.canvas.remove()
  }
  if (runner) {
    $matter.Runner.stop(runner)
  }
})
</script>

<template>
  <div ref="scene" class="physics-container"></div>
</template>

<style scoped>
.physics-container {
  display: flex;
  justify-content: center;
  align-items: center;
}
</style>

Best Practices