TresJS is the renderer that lets you write a Three.js scene as a Vue template: a TresMesh with a TresBoxGeometry child instead of new Mesh(new BoxGeometry()), props instead of property assignments, and reactivity for free. Version 5.0.0 of @tresjs/core reached npm on 20 September after a month of alphas, and the Nuxt module @tresjs/nuxt 5.0.0 and the helper library @tresjs/cientos 5.0.0 landed the same day.
In a v4 project your templates mostly keep working, but the render loop, useLoader, pointer events and context access change. The details below come from the release notes and the upgrade guide in the new docs.
What shipped
The release notes group the changes under breaking changes and features. The main breaking changes:
- The package is ESM only. The UMD build is gone.
- The event system is rebuilt on
@pmndrs/pointer-events. Only the first object under the pointer receives an event, and events use the native DOM names:@pointerdown, not@pointer-down. useRenderLoopand itsonLoopare removed. The loop API isuseLoop, withonBeforeRenderandonRender(the after-render hook, formerlyonAfterRender).useLoaderreturns reactive state instead of a promise.useTexturemoved to@tresjs/cientos.useTresContextstays for module authors and now exposes a camera manager rather than a camera.- Removed composables:
useTresReadyandonTresReady,useSeek,useTresEventManager,useRaycaster,useLogger,useCamera.
The new features:
useTresis now its own composable rather than an alias ofuseTresContext. It gives components the scene, renderer, active camera, sizes,invalidateandadvance.- Native WebGPU rendering through the
rendererprop onTresCanvas. - A context inspector in the devtools, and a warning when the canvas has no size.
The package declares vue >=3.4 and three >=0.133 as peer dependencies. Three.js is at 0.180, published on 3 September, and that is the version to install alongside.
Build a spinning box with useLoop
Start from a Vite project and install the renderer, Three.js and its types:
npm install @tresjs/core three
npm install -D @types/three
The Vue compiler has to be told that Tres* tags are elements for the TresJS renderer, not Vue components to look up. @tresjs/core exports the options:
import { templateCompilerOptions } from '@tresjs/core'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [vue({ ...templateCompilerOptions })],
})
The canvas lives in one component and the scene in another. That split is what the docs recommend, and it is also what makes useLoop work, because the loop is provided by TresCanvas to its descendants:
<script setup lang="ts">
import { TresCanvas } from '@tresjs/core'
import SpinningBox from './components/SpinningBox.vue'
</script>
<template>
<TresCanvas clear-color="#1f2a44" window-size>
<TresPerspectiveCamera :position="[3, 3, 3]" :look-at="[0, 0, 0]" />
<TresAmbientLight :intensity="0.6" />
<TresDirectionalLight :position="[2, 4, 3]" :intensity="1.5" />
<SpinningBox />
</TresCanvas>
</template>
Every Tres element maps to the Three.js class of the same name without the prefix: TresPerspectiveCamera is PerspectiveCamera, TresAmbientLight is AmbientLight. Props set properties on the instance, and args passes constructor arguments. window-size makes the canvas fill the viewport instead of its parent.
The box is a child component that turns itself a little on every frame:
<script setup lang="ts">
import { useLoop } from '@tresjs/core'
import { shallowRef } from 'vue'
import type { Mesh } from 'three'
const box = shallowRef<Mesh>()
const { onBeforeRender } = useLoop()
onBeforeRender(({ delta }) => {
if (!box.value) return
box.value.rotation.y += delta
box.value.rotation.x += delta * 0.5
})
</script>
<template>
<TresMesh ref="box">
<TresBoxGeometry :args="[1, 1, 1]" />
<TresMeshStandardMaterial color="#42b883" />
</TresMesh>
</template>
onBeforeRender runs once per frame before the renderer draws. Its argument carries delta, the seconds since the previous frame, and elapsed, the seconds since the loop started, along with the renderer, scene, camera and sizes. Multiplying by delta keeps the rotation speed the same at 60 and 120 frames per second.
The template ref holds the real Mesh, so anything you know from Three.js works on it. It stays empty until the mesh mounts, hence the if. A shallowRef is the right container because it stores the mesh as it is; a plain ref would hand you a deeply reactive proxy of a large Three.js object that nothing needs to track.
Control the render loop
useLoop returns onBeforeRender, onRender, render, start, stop and isActive. Both hooks take an optional priority number, default 0, and run in ascending order, so a camera follow can be registered at 10 to run after every object has moved at 0. Each call returns an object with off() to unregister the hook.
render hands you the whole frame. You call renderer.render() yourself, or run an effect composer, and you must call the notifySuccess callback it gives you so the render modes keep working.
The render mode is a TresCanvas prop:
render-mode="always", the default, draws every frame."on-demand"draws only after something callsinvalidate(). A scene that only changes on user input saves battery this way, anduseTres()gives any component theinvalidatefunction."manual"draws when you calladvance().
If you prefer to keep the per-frame code in the component that owns the canvas, TresCanvas emits a loop event you can bind with @loop. It also emits a ready event with the context once the renderer exists, which replaces useTresReady and onTresReady.
What breaks for v4 users
The upgrade guide rates each change by impact. Here are the ones that touch most projects, with the fix.
In a component inside the canvas, the loop rename is mechanical:
import { useRenderLoop } from '@tresjs/core'
const { onLoop } = useRenderLoop()
onLoop(({ delta }) => {
if (!box.value) return
box.value.rotation.y += delta
})
import { useLoop } from '@tresjs/core'
const { onBeforeRender } = useLoop()
onBeforeRender(({ delta }) => {
if (!box.value) return
box.value.rotation.y += delta
})
useLoader changes shape rather than name. In v4 you awaited it; in v5 you read its state:
<script setup lang="ts">
import { useLoader } from '@tresjs/core'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
const { state: model, isLoading, error } = useLoader(GLTFLoader, '/models/duck.gltf')
</script>
<template>
<primitive v-if="model?.scene" :object="model.scene" />
</template>
No await, no Suspense needed, and a progress object with loaded, total and percentage if you want a loading bar. Passing a ref as the path reloads when it changes, and a load method loads another path by hand. Textures follow the same pattern after npm install @tresjs/cientos and import { useTexture } from '@tresjs/cientos'.
Pointer events need two edits. Rename dash-case handlers to the native names (@pointer-down becomes @pointerdown, @pointer-enter becomes @pointerenter). Then check any place where two overlapping meshes both had handlers: only the front one fires now, so move the handler to a parent TresGroup if both need it.
For context access, most components only need useTresContext() changed to useTres(). One difference: useTres() gives you the renderer itself rather than a ref, so renderer.value.info becomes renderer.info. If you stay on useTresContext, camera is now a manager object and the active camera is camera.activeCamera.value, the renderer is read-only, and the performance state (perf) is gone.
A quieter one: several TresCanvas props stopped being reactive because they configure the WebGL context at creation. antialias, alpha, depth, stencil, power-preference and friends are read once. shadows, clear-color, tone-mapping, dpr and render-mode still react.
What stays the same
The component model is untouched. Tres elements still map to Three.js classes, args still passes constructor arguments, v-if and v-for still add and remove objects from the scene, and template refs still give you the instance. TresCanvas keeps its props for colour, shadows, tone mapping and pixel ratio. Cientos, the helper library with OrbitControls, environments and shapes, moved to 5.0 in step and keeps its component names, apart from SVG, which is now UseSVG. If your scene is components and templates, which is the point of TresJS, most of it compiles unchanged.
Add TresJS to a Nuxt app
The Nuxt module does the compiler setup, keeps TresCanvas client-only without ClientOnly wrappers, and auto-imports the composables and components of every @tresjs/* package in your project:
npm install three @tresjs/nuxt
export default defineNuxtConfig({
modules: ['@tresjs/nuxt'],
})
@tresjs/nuxt 5.0.0 pins @tresjs/core 5.0.0 and declares support for Nuxt 3.16 and later, so a Nuxt 4 project and a recent Nuxt 3 one can both use it. Install @tresjs/cientos next to it and its components are auto-imported too.
Upgrade now or wait
Upgrade now if you are starting a project or your v4 scenes are mostly templates with a loop or two. Most of the renames are search and replace, and the new loop and loader are easier to teach and to read.
Plan more time for the upgrade if you lean on useSeek, useRaycaster or custom event handling across overlapping meshes, because those need rethinking rather than renaming. For useSeek, the upgrade guide points to useGraph, which finds a loaded model's nodes and materials by name. Wait a release or two if you depend on a TresJS ecosystem package that has not published a 5.0 yet. When you go:
npm install @tresjs/core@5 @tresjs/cientos@5 three@latest
npm install -D @types/three@latest
Then search the project for useRenderLoop, onLoop, useTexture, useTresReady, onTresReady, useSeek, useTresContext, @pointer-, and every await useLoader. Each hit maps to one of the fixes above.