
A hands-on walkthrough of TresJS, the Vue custom renderer for Three.js. From a blank canvas to a reactive 3D scene.
Reza Baar
June 10, 2026
TresJS is a Vue custom renderer that lets you build Three.js scenes using Vue components and composables. Instead of writing imperative Three.js code, you declare your scene in the template the same way you'd build any other Vue UI. In this post, we'll go from a blank canvas to a scene with geometry, materials, lighting, controls, and reactive state.
Install the core package and Three.js:
npm install @tresjs/core three
TresJS also has a companion library called Cientos that provides ready-made abstractions (orbit controls, loaders, helpers). We'll use it later:
npm install @tresjs/cientos
Every TresJS scene starts with <TresCanvas>. This component sets up the WebGL renderer, creates a scene, and starts the render loop.
<!-- App.vue -->
<script setup lang="ts">
import { TresCanvas } from '@tresjs/core';
</script>
<template>
<TresCanvas clear-color="#1a1a2e" window-size>
<TresPerspectiveCamera :position="[0, 2, 5]" />
</TresCanvas>
</template>
clear-color sets the background. window-size makes the canvas fill the viewport. The TresPerspectiveCamera is positioned slightly above and back from the origin so we can see what we're building.
That's it for the boilerplate. Everything else we add goes inside <TresCanvas> as child components.
A mesh in Three.js is a combination of geometry (the shape) and material (the appearance). In TresJS, you compose them as nested components:
<template>
<TresCanvas clear-color="#1a1a2e" window-size>
<TresPerspectiveCamera :position="[0, 2, 5]" />
<TresMesh>
<TresBoxGeometry :args="[1, 1, 1]" />
<TresMeshStandardMaterial color="#4fc3f7" />
</TresMesh>
</TresCanvas>
</template>
TresBoxGeometry creates a 1x1x1 cube. The :args prop maps directly to the Three.js constructor arguments. TresMeshStandardMaterial is a physically-based material that responds to light.
You won't see much yet because we haven't added any lights. The cube will appear black.
Let's add an ambient light (fills the whole scene softly) and a directional light (simulates sunlight from one direction):
<template>
<TresCanvas clear-color="#1a1a2e" window-size>
<TresPerspectiveCamera :position="[0, 2, 5]" />
<TresAmbientLight :intensity="0.4" />
<TresDirectionalLight :position="[3, 5, 2]" :intensity="1.2" />
<TresMesh>
<TresBoxGeometry :args="[1, 1, 1]" />
<TresMeshStandardMaterial color="#4fc3f7" />
</TresMesh>
</TresCanvas>
</template>
Now the cube is visible with shading. The directional light creates shadows on the faces based on its position.
You'll notice we didn't import TresMesh, TresBoxGeometry, or any of the other scene components. TresJS auto-generates a Vue component for every Three.js class. The naming convention is:
BoxGeometry, MeshStandardMaterial, DirectionalLight)Tres prefixSo THREE.SphereGeometry becomes <TresSphereGeometry>, THREE.PointLight becomes <TresPointLight>, and so on. This works for every class in the Three.js library.
To rotate around the scene with the mouse, we need orbit controls. This comes from the Cientos package:
<script setup lang="ts">
import { TresCanvas } from '@tresjs/core';
import { OrbitControls } from '@tresjs/cientos';
</script>
<template>
<TresCanvas clear-color="#1a1a2e" window-size>
<TresPerspectiveCamera :position="[0, 2, 5]" />
<OrbitControls />
<TresAmbientLight :intensity="0.4" />
<TresDirectionalLight :position="[3, 5, 2]" :intensity="1.2" />
<TresMesh>
<TresBoxGeometry :args="[1, 1, 1]" />
<TresMeshStandardMaterial color="#4fc3f7" />
</TresMesh>
</TresCanvas>
</template>
Now you can click and drag to rotate, scroll to zoom, and right-click to pan.
This is where TresJS really shines. Because it's a Vue renderer, every prop is reactive. If you bind a ref to a position, color, or scale, the 3D scene updates automatically.
Let's make the cube's color and scale reactive:
<script setup lang="ts">
import { TresCanvas } from '@tresjs/core';
import { OrbitControls } from '@tresjs/cientos';
const cubeColor = ref('#4fc3f7');
const cubeScale = ref(1);
</script>
<template>
<div>
<div class="controls">
<input type="color" v-model="cubeColor" />
<input type="range" v-model.number="cubeScale" min="0.5" max="3" step="0.1" />
</div>
<TresCanvas clear-color="#1a1a2e" window-size>
<TresPerspectiveCamera :position="[0, 2, 5]" />
<OrbitControls />
<TresAmbientLight :intensity="0.4" />
<TresDirectionalLight :position="[3, 5, 2]" :intensity="1.2" />
<TresMesh :scale="cubeScale">
<TresBoxGeometry :args="[1, 1, 1]" />
<TresMeshStandardMaterial :color="cubeColor" />
</TresMesh>
</TresCanvas>
</div>
</template>
Change the color picker or drag the slider, and the cube updates in real time. No imperative Three.js calls, no manual render triggers. Vue's reactivity system drives the 3D scene the same way it drives the DOM.
For continuous animation (like rotating the cube), TresJS provides useRenderLoop. This composable gives you a callback that fires on every frame:
<script setup lang="ts">
import { TresCanvas, useRenderLoop } from '@tresjs/core';
import { OrbitControls } from '@tresjs/cientos';
import type { Mesh } from 'three';
const cubeRef = ref<Mesh | null>(null);
const { onLoop } = useRenderLoop();
onLoop(({ delta }) => {
if (cubeRef.value) {
cubeRef.value.rotation.y += delta * 0.5;
cubeRef.value.rotation.x += delta * 0.2;
}
});
</script>
<template>
<TresCanvas clear-color="#1a1a2e" window-size>
<TresPerspectiveCamera :position="[0, 2, 5]" />
<OrbitControls />
<TresAmbientLight :intensity="0.4" />
<TresDirectionalLight :position="[3, 5, 2]" :intensity="1.2" />
<TresMesh ref="cubeRef">
<TresBoxGeometry :args="[1, 1, 1]" />
<TresMeshStandardMaterial color="#4fc3f7" />
</TresMesh>
</TresCanvas>
</template>
We get a template ref to the mesh, then rotate it each frame. The delta value is the time since the last frame, so the rotation speed stays consistent regardless of frame rate.
Let's put it all together into something more interesting. We'll create a scene with a ground plane, multiple objects, and some variety:
<script setup lang="ts">
import { TresCanvas, useRenderLoop } from '@tresjs/core';
import { OrbitControls } from '@tresjs/cientos';
import type { Group } from 'three';
const groupRef = ref<Group | null>(null);
const { onLoop } = useRenderLoop();
onLoop(({ delta }) => {
if (groupRef.value) {
groupRef.value.rotation.y += delta * 0.15;
}
});
</script>
<template>
<TresCanvas clear-color="#0f0f23" window-size shadows>
<TresPerspectiveCamera :position="[4, 3, 6]" />
<OrbitControls />
<TresAmbientLight :intensity="0.3" />
<TresDirectionalLight
:position="[5, 8, 3]"
:intensity="1.5"
cast-shadow
/>
<!-- Ground plane -->
<TresMesh :rotation="[-Math.PI / 2, 0, 0]" receive-shadow>
<TresPlaneGeometry :args="[10, 10]" />
<TresMeshStandardMaterial color="#2a2a4a" />
</TresMesh>
<!-- Rotating group of objects -->
<TresGroup ref="groupRef">
<!-- Cube -->
<TresMesh :position="[-1.5, 0.5, 0]" cast-shadow>
<TresBoxGeometry :args="[1, 1, 1]" />
<TresMeshStandardMaterial color="#4fc3f7" />
</TresMesh>
<!-- Sphere -->
<TresMesh :position="[1.5, 0.6, 0]" cast-shadow>
<TresSphereGeometry :args="[0.6, 32, 32]" />
<TresMeshStandardMaterial color="#f06292" :metalness="0.3" :roughness="0.4" />
</TresMesh>
<!-- Torus -->
<TresMesh :position="[0, 1.2, -1.5]" cast-shadow>
<TresTorusGeometry :args="[0.5, 0.2, 16, 32]" />
<TresMeshStandardMaterial color="#81c784" :metalness="0.5" :roughness="0.2" />
</TresMesh>
</TresGroup>
</TresCanvas>
</template>
A few things to note here. We pass shadows to <TresCanvas> to enable the shadow map. Individual meshes get cast-shadow or receive-shadow. The <TresGroup> lets us rotate all three objects as a unit. The ground plane is rotated -90 degrees on the X axis to lay flat.
TresJS supports pointer events on meshes, just like DOM events on HTML elements:
<TresMesh
:position="[0, 0.5, 0]"
@click="handleClick"
@pointer-enter="handleHover"
@pointer-leave="handleLeave"
>
<TresBoxGeometry :args="[1, 1, 1]" />
<TresMeshStandardMaterial :color="isHovered ? '#ff9800' : '#4fc3f7'" />
</TresMesh>
const isHovered = ref(false);
function handleClick() {
console.log('Cube clicked!');
}
function handleHover() {
isHovered.value = true;
}
function handleLeave() {
isHovered.value = false;
}
The color updates reactively on hover. Under the hood, TresJS uses raycasting to detect which mesh the pointer intersects with.
We used OrbitControls from Cientos, but the package includes a lot more:
useGLTF for loading 3D models, useTexture for imagesStars, Sky, Environment for scene backgroundsTransformControls, PointerLockControlsGrid, StatsGl for performance monitoringLoading a 3D model, for example:
<script setup lang="ts">
import { useGLTF } from '@tresjs/cientos';
const { scene: model } = await useGLTF('/models/robot.glb');
</script>
<template>
<primitive :object="model" :scale="0.5" />
</template>
useGLTF is async, so it works with Vue's <Suspense> if you need a loading fallback.
Tres + Three.js class name in PascalCaseuseRenderLoop gives you a per-frame callback for animations@click, @pointer-enter) work on meshes like DOM eventsv-for, v-if, computed props, watchersI hope this post has been helpful. Enjoy making 3D scenes and happy coding!
Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.

Error Handling in Next.js with catchError
Learn why react-error-boundary falls short in the Next.js App Router and how catchError from Next.js 16.2 fixes both framework error propagation and server data refetching with a single function call.
Aurora Scharff
Jun 18, 2026

SEO in Nuxt with @nuxtjs/seo
Set up sitemaps, meta tags, structured data, OG images, and robots.txt in Nuxt with the official SEO module.
Reza Baar
Jun 17, 2026
![What’s the untracked function? [Angular Signals]](/.netlify/images?url=https:%2F%2Fapi.certificates.dev%2Fstorage%2FZzk75tZNAVT5d3GI9TxAD2JwkIFUKavFFj8sC2BL.png)
What’s the untracked function? [Angular Signals]
Learn how Angular's computed() function derives reactive values from signals and why it plays a key role in building high-performance, signal-based applications with cleaner and more predictable state management.
Alain Chautard
Jun 16, 2026