---
title: "TresJS 5: Three.js scenes as Vue components · vuejs.school"
description: "TresJS 5.0 is ESM only, rebuilds pointer events, replaces useRenderLoop with useLoop and makes useLoader reactive. The scene, the loop and the upgrade."
canonical_url: "https://vuejs.school/community/blog/tresjs-5-three-js-scenes-as-vue-components"
meta:
  "og:description": "TresJS 5.0 is ESM only, rebuilds pointer events, replaces useRenderLoop with useLoop and makes useLoader reactive. The scene, the loop and the upgrade."
  "og:image": "https://vuejs.school/_og/d/a_~VHJlc0pTIDU6IFRocmVlLmpzIHNjZW5lcyBhcyBWdWUgY29tcG9uZW50cyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VHJlc0pTIDU6IFRocmVlLmpzIHNjZW5lcyBhcyBWdWUgY29tcG9uZW50cw%2Cdescription_~VHJlc0pTIDUuMCBpcyBFU00gb25seSwgcmVidWlsZHMgcG9pbnRlciBldmVudHMsIHJlcGxhY2VzIHVzZVJlbmRlckxvb3Agd2l0aCB1c2VMb29wIGFuZCBtYWtlcyB1c2VMb2FkZXIgcmVhY3RpdmUuIFRoZSBzY2VuZSwgdGhlIGxvb3AgYW5kIHRoZSB1cGdyYWRlLg%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA3IG1pbiByZWFk%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90cmVzanMtNS10aHJlZS1qcy1zY2VuZXMtYXMtdnVlLWNvbXBvbmVudHMi%2Cs_baGU3uR0-dmozays.png"
  "og:image:alt": "TresJS 5: Three.js scenes as Vue components · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "TresJS 5: Three.js scenes as Vue components · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~VHJlc0pTIDU6IFRocmVlLmpzIHNjZW5lcyBhcyBWdWUgY29tcG9uZW50cyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VHJlc0pTIDU6IFRocmVlLmpzIHNjZW5lcyBhcyBWdWUgY29tcG9uZW50cw%2Cdescription_~VHJlc0pTIDUuMCBpcyBFU00gb25seSwgcmVidWlsZHMgcG9pbnRlciBldmVudHMsIHJlcGxhY2VzIHVzZVJlbmRlckxvb3Agd2l0aCB1c2VMb29wIGFuZCBtYWtlcyB1c2VMb2FkZXIgcmVhY3RpdmUuIFRoZSBzY2VuZSwgdGhlIGxvb3AgYW5kIHRoZSB1cGdyYWRlLg%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA3IG1pbiByZWFk%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90cmVzanMtNS10aHJlZS1qcy1zY2VuZXMtYXMtdnVlLWNvbXBvbmVudHMi%2Cs_baGU3uR0-dmozays.png"
  "twitter:image:alt": "TresJS 5: Three.js scenes as Vue components · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## 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`.
- `useRenderLoop` and its `onLoop` are removed. The loop API is `useLoop`, with `onBeforeRender` and `onRender` (the after-render hook, formerly `onAfterRender`).
- `useLoader` returns reactive state instead of a promise.
- `useTexture` moved to `@tresjs/cientos`.
- `useTresContext` stays for module authors and now exposes a camera manager rather than a camera.
- Removed composables: `useTresReady` and `onTresReady`, `useSeek`, `useTresEventManager`, `useRaycaster`, `useLogger`, `useCamera`.

The new features:

- `useTres` is now its own composable rather than an alias of `useTresContext`. It gives components the scene, renderer, active camera, sizes, `invalidate` and `advance`.
- Native WebGPU rendering through the `renderer` prop on `TresCanvas`.
- 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:

*vite.config.ts*

```
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:

*src/App.vue*

```
<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:

*src/components/SpinningBox.vue*

```
<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 calls `invalidate()`. A scene that only changes on user input saves battery this way, and `useTres()` gives any component the `invalidate` function.
- `"manual"` draws when you call `advance()`.

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:

*SpinningBox.vue**Breaks***

```
import { useRenderLoop } from '@tresjs/core'

const { onLoop } = useRenderLoop()
onLoop(({ delta }) => {
  if (!box.value) return
  box.value.rotation.y += delta
})
```

*SpinningBox.vue**Works***

```
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:

*Duck.vue*

```
<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
```

*nuxt.config.ts*

```
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.

**Check it stuck**3 questions from the 3D scenes with TresJS quiz.

[**Practice now**](https://vuejs.school/learn/graphics/3d-scenes-with-tresjs?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 6 min read**A spinning 3D scene with TresJS and useLoop()**](https://vuejs.school/community/blog/a-spinning-3d-scene-with-tresjs-and-useloop) [Tutorial · 6 min read**Canvas and animation loops in a Vue component**](https://vuejs.school/community/blog/canvas-and-animation-loops-in-a-vue-component)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/tresjs-5-three-js-scenes-as-vue-components#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-09-25","description":"TresJS 5.0 is ESM only, rebuilds pointer events, replaces useRenderLoop with useLoop and makes useLoader reactive. The scene, the loop and the upgrade.","name":"TresJS 5: Three.js scenes as Vue components","url":"https://vuejs.school/community/blog/tresjs-5-three-js-scenes-as-vue-components","breadcrumb":{"@id":"https://vuejs.school/community/blog/tresjs-5-three-js-scenes-as-vue-components#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/tresjs-5-three-js-scenes-as-vue-components"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/tresjs-5-three-js-scenes-as-vue-components#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-09-25","description":"TresJS 5.0 is ESM only, rebuilds pointer events, replaces useRenderLoop with useLoop and makes useLoader reactive. The scene, the loop and the upgrade.","headline":"TresJS 5: Three.js scenes as Vue components","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~VHJlc0pTIDU6IFRocmVlLmpzIHNjZW5lcyBhcyBWdWUgY29tcG9uZW50cyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VHJlc0pTIDU6IFRocmVlLmpzIHNjZW5lcyBhcyBWdWUgY29tcG9uZW50cw%2Cdescription_~VHJlc0pTIDUuMCBpcyBFU00gb25seSwgcmVidWlsZHMgcG9pbnRlciBldmVudHMsIHJlcGxhY2VzIHVzZVJlbmRlckxvb3Agd2l0aCB1c2VMb29wIGFuZCBtYWtlcyB1c2VMb2FkZXIgcmVhY3RpdmUuIFRoZSBzY2VuZSwgdGhlIGxvb3AgYW5kIHRoZSB1cGdyYWRlLg%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA3IG1pbiByZWFk%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90cmVzanMtNS10aHJlZS1qcy1zY2VuZXMtYXMtdnVlLWNvbXBvbmVudHMi%2Cs_baGU3uR0-dmozays.png","@type":["Article","BlogPosting"],"articleSection":["Release notes"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/tresjs-5-three-js-scenes-as-vue-components#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/tresjs-5-three-js-scenes-as-vue-components#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/tresjs-5-three-js-scenes-as-vue-components#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"TresJS 5: Three.js scenes as Vue components","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~VHJlc0pTIDU6IFRocmVlLmpzIHNjZW5lcyBhcyBWdWUgY29tcG9uZW50cyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VHJlc0pTIDU6IFRocmVlLmpzIHNjZW5lcyBhcyBWdWUgY29tcG9uZW50cw%2Cdescription_~VHJlc0pTIDUuMCBpcyBFU00gb25seSwgcmVidWlsZHMgcG9pbnRlciBldmVudHMsIHJlcGxhY2VzIHVzZVJlbmRlckxvb3Agd2l0aCB1c2VMb29wIGFuZCBtYWtlcyB1c2VMb2FkZXIgcmVhY3RpdmUuIFRoZSBzY2VuZSwgdGhlIGxvb3AgYW5kIHRoZSB1cGdyYWRlLg%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA3IG1pbiByZWFk%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90cmVzanMtNS10aHJlZS1qcy1zY2VuZXMtYXMtdnVlLWNvbXBvbmVudHMi%2Cs_baGU3uR0-dmozays.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~VHJlc0pTIDU6IFRocmVlLmpzIHNjZW5lcyBhcyBWdWUgY29tcG9uZW50cyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VHJlc0pTIDU6IFRocmVlLmpzIHNjZW5lcyBhcyBWdWUgY29tcG9uZW50cw%2Cdescription_~VHJlc0pTIDUuMCBpcyBFU00gb25seSwgcmVidWlsZHMgcG9pbnRlciBldmVudHMsIHJlcGxhY2VzIHVzZVJlbmRlckxvb3Agd2l0aCB1c2VMb29wIGFuZCBtYWtlcyB1c2VMb2FkZXIgcmVhY3RpdmUuIFRoZSBzY2VuZSwgdGhlIGxvb3AgYW5kIHRoZSB1cGdyYWRlLg%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA3IG1pbiByZWFk%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90cmVzanMtNS10aHJlZS1qcy1zY2VuZXMtYXMtdnVlLWNvbXBvbmVudHMi%2Cs_baGU3uR0-dmozays.png"}]}
```