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-js2. 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
- Always Guard with
onMounted: Even when using a.client.tsplugin, Vue components render on the server by default. Accessing physics elements must strictly occur withinonMountedor inside Nuxt's<ClientOnly>wrapper. - Tear Down Listeners and Canvases: Always invoke
Render.stop()andRunner.stop()during component teardown to prevent orphanedrequestAnimationFrameloops when switching routes.