Html ^3.5.0
This component allows you to project HTML content to any object in your scene. TresJS will automatically update the position of the HTML content to match the position of the object in the scene.
🚀 Works seamlessly with both PerspectiveCamera and OrthographicCamera — the active camera is automatically detected by the <Html> component.
Usage
<script setup lang="ts">
import { Html, OrbitControls } from '@tresjs/cientos'
import { TresCanvas } from '@tresjs/core'
const gl = {
clearColor: '#82DBC5',
}
</script>
<template>
<TresCanvas v-bind="gl">
<TresPerspectiveCamera :position="[3, 3, 8]" />
<OrbitControls />
<TresMesh :position="[0, .5, 0]">
<TresBoxGeometry :args="[1.5, 1.5, 1.5]" />
<TresMeshNormalMaterial />
<Html
center
transform
:z-index-range="[28, 0]"
:position="[0, 0, 1]"
:scale="[0.65, 0.65, 0.65]"
>
<h1 class="bg-white dark:bg-dark text-xs p-1 rounded">
I'm an HTML Box 📦
</h1>
</Html>
</TresMesh>
<TresAmbientLight />
</TresCanvas>
</template>Occlusion
By default, the HTML content will be visible through other objects in the scene. You can use the occlude prop to make the HTML content occlude other objects in the scene.
Html can be hidden behind one or more objects in your scene using the occlude prop.
<Html occlude>If occlude, then <Html> will be hidden by any objects that pass in front of its position.
Demo code
<script setup lang="ts">
import { Html, OrbitControls } from '@tresjs/cientos'
import { TresCanvas } from '@tresjs/core'
const gl = {
clearColor: '#82DBC5',
}
</script>
<template>
<TresCanvas v-bind="gl">
<TresPerspectiveCamera :position="[7, 2, 5]" />
<OrbitControls auto-rotate :auto-rotate-speed="3" />
<TresMesh :position="[0, 1, -2]">
<TresBoxGeometry />
<TresMeshNormalMaterial />
<Html
center
transform
occlude
:distance-factor="4"
:position="[0, 0, 2]"
:z-index-range="[28, 0]"
>
<h1 class="bg-white dark:bg-dark text-xs p-1 rounded">
Move camera
</h1>
</Html>
</TresMesh>
<TresMesh
:position="[0, 1, 2]"
>
<TresSphereGeometry />
<TresMeshNormalMaterial />
</TresMesh>
<TresGridHelper />
<TresAmbientLight :intensity="1" />
</TresCanvas>
</template>You can also choose which object or objects should occlude the HTML content by passing either a single object ref or an array of object refs to the occlude prop:
Single occluder
<Html occlude="[mesh]">Demo code
<script setup lang="ts">
import { Html, OrbitControls } from '@tresjs/cientos'
import { TresCanvas } from '@tresjs/core'
import { ref } from 'vue'
const gl = {
clearColor: '#82DBC5',
}
const sphereRef = ref(null)
</script>
<template>
<TresCanvas v-bind="gl">
<TresPerspectiveCamera :position="[3, 3, 8]" />
<OrbitControls />
<TresMesh :position="[0, 1, 0]">
<TresBoxGeometry />
<TresMeshNormalMaterial />
<Html
center
transform
:occlude="[sphereRef]"
:distance-factor="4"
:z-index-range="[28, 0]"
>
<h1 class="bg-white dark:bg-dark text-xs p-1 rounded">
Move camera
</h1>
</Html>
</TresMesh>
<TresMesh
ref="sphereRef"
:position="[3, 1, 1]"
>
<TresSphereGeometry />
<TresMeshNormalMaterial />
</TresMesh>
<TresGridHelper />
<TresAmbientLight :intensity="1" />
</TresCanvas>
</template>Multiple occluders
<Html occlude="[mesh1, mesh2, mesh3, ...]" />OR
<Html occlude="meshesArray" />In the demo below, a v-for loop generates multiple spheres around the cube. All resulting Mesh instances are collected into an array and passed to the occlude prop, allowing each sphere to occlude the HTML content.
This demo also uses the on-occlude event, which is triggered whenever the occlusion state changes. Here, the event updates a reactive value to control element styles — for example, toggling between light and dark themes.
Demo code
<script setup lang="ts">
import { Html, OrbitControls } from '@tresjs/cientos'
import type { TresObject3D } from '@tresjs/core'
import { TresCanvas } from '@tresjs/core'
import { computed, ref, toRaw, useTemplateRef } from 'vue'
const gl = {
clearColor: '#82DBC5',
}
const htmlProps = {
center: true,
transform: true,
sprite: true,
}
const count = 4
const radius = 4
const isOccluded = ref(false)
const spheresParams = computed(() =>
Array.from({ length: count }, (_, i) => {
const angle = (i * 2 * Math.PI) / count
const x = Math.cos(angle) * radius
const z = Math.sin(angle) * radius
return { position: [x, 1, z] as [number, number, number] }
}),
)
const spheresOccludeRef = useTemplateRef<TresObject3D[]>('spheresOcclude')
const occluderRefs = computed<TresObject3D[]>(() => {
const arr = spheresOccludeRef.value ?? []
return arr.map((occluder: TresObject3D) => toRaw(occluder))
})
</script>
<template>
<TresCanvas v-bind="gl">
<TresPerspectiveCamera :position="[3.5, 2.5, 9.5]" />
<OrbitControls />
<TresMesh :position="[0, 1, 0]">
<TresBoxGeometry />
<TresMeshNormalMaterial />
<Html
v-bind="htmlProps"
:occlude="occluderRefs"
:distance-factor="4"
:z-index-range="[28, 0]"
@on-occlude="(event: boolean) => isOccluded = event"
>
<h1 class="bg-white dark:bg-dark text-xs p-1 rounded">
Move camera
</h1>
</Html>
</TresMesh>
<TresMesh
v-for="(sphere, index) in spheresParams"
:key="`html-demo-occlude-complex-${index}`"
ref="spheresOcclude"
:position="sphere.position"
>
<TresSphereGeometry />
<TresMeshNormalMaterial />
<Html
v-bind="htmlProps"
:distance-factor="4"
:z-index-range="[28, 0]"
>
<h1 class="text-xs p-1 rounded" :class="[isOccluded ? 'bg-white text-dark' : 'bg-dark text-white']">
Occlude {{ index + 1 }}
</h1>
</Html>
</TresMesh>
<TresGridHelper />
<TresAmbientLight :intensity="1" />
</TresCanvas>
</template>Blending Occlusion
<Html> can hide behind geometry as if it was part of the 3D scene using this mode. It can be enabled by using "blending" as the occlude prop.
<Html occlude="blending">The demo below ⬇️ (left black example) shows a basic usage example.
Demo code
<script setup lang="ts">
import { Html, Levioso, OrbitControls } from '@tresjs/cientos'
import { TresCanvas } from '@tresjs/core'
import { shallowRef } from 'vue'
import { CircleGeometry, MeshStandardMaterial } from 'three'
const gl = {
clearColor: '#82DBC5',
clearAlpha: 1,
shadows: true,
alpha: true,
}
const targetDirectionLightRef = shallowRef(null)
const geometries = [
{
component: 'TresBoxGeometry',
args: [1, 1, 1],
},
{
component: 'TresSphereGeometry',
args: [0.7, 32, 32],
},
{
component: 'TresTorusGeometry',
args: [0.5, 0.2, 16, 100],
bind: { castShadow: true, receiveShadow: true },
},
]
const customGeometry = shallowRef(new CircleGeometry(1.25, 32))
const customMaterial = shallowRef(new MeshStandardMaterial({
color: 'red',
side: 2,
opacity: 1,
transparent: true,
}))
</script>
<template>
<div class="html-demo-wrapper">
<TresCanvas v-bind="gl">
<TresPerspectiveCamera :position="[0, 1.5, 7.5]" />
<OrbitControls />
<Levioso
v-for="(geometry, index) in geometries"
:key="`html-occlude-blending-demo-${index}`"
:speed="3"
:float-factor="3.5"
:rotation-factor="1"
:range="[-0.35, 0.35]"
>
<TresMesh :position="[index * 3.5 - 3.5, 1, 0]" v-bind="geometry.bind">
<component :is="geometry.component" :args="geometry.args" />
<TresMeshNormalMaterial />
</TresMesh>
</Levioso>
<Html
center
transform
occlude="blending"
:position="[-4, .75, -2]"
:z-index-range="[28, 0]"
>
<div class="text-center text-s p-2 bg-[#1B1C1E] text-light">
BASIC 💛 <br />
<em>occlude=blending</em>
</div>
</Html>
<Html
center
transform
occlude="blending"
:position="[0, .85, -2]"
:geometry="customGeometry"
:z-index-range="[28, 0]"
>
<div class="text-xs p-8 text-center bg-[#F6B03B] text-dark">
CUSTOM <br /> <strong>CIRCLE <br /> GEOMETRY</strong>
</div>
</Html>
<Html
ref="targetDirectionLightRef"
center
transform
occlude="blending"
:position="[4, .5, -2]"
:material="customMaterial"
receive-shadow
:z-index-range="[28, 0]"
>
<div style="width: 100px; height: auto; aspect-ratio: 250/250;"></div>
</Html>
<Html
center
transform
occlude="blending"
:position="[4, 2.5, -2]"
:z-index-range="[28, 0]"
>
<div class="text-center text-xs p-2 text-dark bg-[#FF0000]">
<strong>HTML + Custom material </strong> <br />
<em>+ receive-shadow </em> ⬇️
</div>
</Html>
<TresDirectionalLight
v-if="targetDirectionLightRef?.instance"
:target="targetDirectionLightRef?.instance"
:shadow-normalBias="0.075"
:position="[5, 0, 5]"
:intensity="2"
cast-shadow
/>
<TresGridHelper :position-y="-1" />
<TresAmbientLight :intensity="1" />
</TresCanvas>
</div>
</template>
<style scoped>
.html-demo-wrapper {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
background-color: #82dbc5;
}
</style>Custom Geometry
By default, when using occlude="blending", occlusion works correctly only with rectangular HTML elements (using a PlaneGeometry). For non-rectangular content, you can use the geometry prop to provide a matching custom geometry.
In the demo above ⬆️ (middle yellow example), a CircleGeometry is used as a custom geometry.
INFO
- The
geometryprop only defines the occlusion shape in 3D and does not modify your HTML content. - You can provide any
BufferGeometry, for example to simulate CSS-like styles such asborder-radiususing a rounded rectangle or squircle geometry (see RoundedRectangle / Squircle geometry for example).
Custom Material
You can also assign material properties to the HTML content using the material prop. In the demo above ⬆️ (right red example), a custom material is used with shadow.
ℹ️ MATERIAL
The material prop is only available when occlude="blending" is enabled.
ℹ️ SHADOW
Enable shadows using the castShadow and receiveShadow props. Shadows are supported only when using a custom material. By default, shadows do not work with MeshBasicMaterial or ShaderMaterial.
Using <Transition>
The native Vue <Transition> component works seamlessly with <Html>. This means you can animate how your projected HTML content enters and leaves the scene, exactly as you would in a regular Vue application.
INFO
All standard interactions are supported just like on a regular HTML element — hover effects, events, and any kind of DOM interaction are fully possible.
Demo code
<script setup lang="ts">
import { Html, Levioso, OrbitControls } from '@tresjs/cientos'
import { TresCanvas } from '@tresjs/core'
import { TresLeches, useControls } from '@tresjs/leches'
import { ref } from 'vue'
import '@tresjs/leches/styles'
const gl = {
clearColor: '#82DBC5',
clearAlpha: 0,
shadows: true,
alpha: true,
antialias: true,
}
const rootRef = ref<HTMLElement>()
const bgColor = ref('#F6B03B')
const geometries = [
{
component: 'TresBoxGeometry',
args: [1, 1, 1],
},
{
component: 'TresSphereGeometry',
args: [0.7, 32, 32],
},
{
component: 'TresTorusGeometry',
args: [0.5, 0.2, 16, 100],
},
{
component: 'TresConeGeometry',
args: [0.7, 1.4, 32],
},
]
const { showTransition } = useControls({
showTransition: true,
})
const getRandomBackgroundColor = () => {
const colors = ['#F6B03B', '#82DBC5', '#FF5733', '#33FF57', '#3357FF', '#F333FF', '#33FFF5']
const randomColor = colors[Math.floor(Math.random() * colors.length)]
return randomColor
}
const updateBackgroundColor = () => {
bgColor.value = getRandomBackgroundColor()
}
</script>
<template>
<div ref="rootRef" class="html-demo-wrapper">
<TresLeches class="important-top-initial important-bottom-4 important-left-4" :style="{ zIndex: 9999999999 }" />
<TresCanvas v-bind="gl" class="!pointer-events-none">
<TresPerspectiveCamera :position="[0, 1.5, 7.5]" />
<OrbitControls :dom-element="rootRef" />
<Levioso
v-for="(geometry, index) in geometries"
:key="`html-occlude-blending-demo-${index}`"
:speed="3"
:float-factor="3.5"
:rotation-factor="1"
:range="[-0.4, 0.4]"
>
<TresMesh :position="[(index - (geometries.length - 1) / 2) * 2, 1, 0]">
<component :is="geometry.component" :args="geometry.args" />
<TresMeshNormalMaterial />
</TresMesh>
</Levioso>
<Html
center
transform
occlude="blending"
:position="[0, .75, -2]"
:scale="1.15"
:z-index-range="[28, 0]"
>
<Transition name="transition-basic">
<h1
v-if="showTransition"
:style="{ backgroundColor: bgColor }"
class="html-demo-transition-heading will-change-transform transition-transform,background-color cursor-pointer duration-500 text-center p-2 text-light shadow-lg"
@click="updateBackgroundColor"
>
<strong>TRANSITION + </strong><br />
<em>occlude=blending 💛</em>
</h1>
</Transition>
</Html>
<TresGridHelper :position-y="-1.25" />
<TresAmbientLight :intensity="1" />
</TresCanvas>
</div>
</template>
<style scoped>
.html-demo-wrapper {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
background-color: #82dbc5;
}
.html-demo-transition-heading:hover {
transform: scale(1.05);
}
.transition-basic-enter-from,
.transition-basic-leave-to {
opacity: 0;
transform: translateY(-20px);
}
.transition-basic-enter-active,
.transition-basic-leave-active {
transition: all 0.5s ease;
}
.transition-basic-enter-to,
.transition-basic-leave-from {
opacity: 1;
transform: translateY(0);
}
</style>Using iframes
You can achieve pretty cool results with the Html component by using iframes. For example, you can use an iframe to display a YouTube video in your scene or a webpage with a 3D model.
Demo code
<script setup lang="ts">
import { ContactShadows, Environment, Html, Levioso, OrbitControls } from '@tresjs/cientos'
import { TresCanvas } from '@tresjs/core'
import { MathUtils } from 'three'
import { ref } from 'vue'
const gl = {
clearColor: '#F6B03B',
shadows: true,
antialias: true,
}
const n = 30
const portalRef = ref<HTMLElement>()
const items = Array.from({ length: n }, () => ({
position: [
MathUtils.randFloat(-10, 10),
MathUtils.randFloat(-2, 6.5),
MathUtils.randFloat(3.5, 8)
],
scale: MathUtils.randFloat(0.25, 0.5),
rotation: [
MathUtils.randFloat(0, Math.PI),
MathUtils.randFloat(0, Math.PI),
0
]
}))
</script>
<template>
<TresCanvas v-bind="gl">
<TresPerspectiveCamera :position="[0, 8.5, 22]" />
<OrbitControls make-default :enable-pan="false" :domElement="portalRef" :maxPolarAngle="Math.PI / 2.2" :target="[0, 2, 0]" />
<Levioso v-for="(item, index) in items" :key="index" :speed="2">
<TresMesh
:position="[item.position[0], item.position[1], item.position[2]]"
:scale="[item.scale, item.scale, item.scale]"
:rotation="[item.rotation[0], item.rotation[1], item.rotation[2]]"
>
<TresMeshStandardMaterial color="white" attach="material" />
<TresTetrahedronGeometry v-if="index % 9 === 0" :args="[2]" />
<TresCylinderGeometry v-else-if="index % 9 === 1" :args="[0.8, 0.8, 2, 32]" />
<TresConeGeometry v-else-if="index % 9 === 2" :args="[1.1, 1.7, 32]" />
<TresSphereGeometry v-else-if="index % 9 === 3" :args="[1.5, 32, 32]" />
<TresIcosahedronGeometry v-else-if="index % 9 === 4" :args="[2]" />
<TresTorusGeometry v-else-if="index % 9 === 5" :args="[1.1, 0.35, 16, 32]" />
<TresOctahedronGeometry v-else-if="index % 9 === 6" :args="[2]" />
<TresSphereGeometry v-else-if="index % 9 === 7" :args="[1.5, 32, 32]" />
<TresBoxGeometry v-else-if="index % 9 === 8" :args="[2.5, 2.5, 2.5]" />
</TresMesh>
</Levioso>
<Levioso :speed="1.5">
<Html
transform
center
:cast-shadow="true"
:receive-shadow="true"
occlude="blending"
:z-index-range="[28, 0]"
:position-y="2.5"
:portal="portalRef"
:style="{ userSelect: 'none' }"
>
<iframe
class="w-[700px] h-[500px]"
src="https://tresjs.org"
frameborder="0"
:width="700"
:height="500"
></iframe>
</Html>
</Levioso>
<ContactShadows
:blur="1"
:opacity="0.85"
:position-y="-5.5"
:scale="30"
:far="25"
/>
<Suspense>
<Environment preset="city" />
</Suspense>
</TresCanvas>
<div ref="portalRef" class="html-tresjs-portal"></div>
</template>
<style scoped>
.html-tresjs-portal {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
canvas {
pointer-events: none !important;
}
</style>INFO
The demos use :z-index-range="[28, 0]" simply to ensure the HTML elements stay below the documentation header (which uses z-index: 30).
This value is for the docs only — you can ignore it or adjust it as needed.
Props
| Prop | Description | Default |
|---|---|---|
| as | Wrapping HTML element. | 'div' |
| wrapperClass | The className of the wrapping element. element. | |
| prepend | Projects content behind the canvas. | false |
| center | Adds a transform: translate(-50%, -50%). ➡️ Ignored in transform mode. | false |
| fullscreen | Aligns to the upper-left corner and fills the screen. ➡️ Ignored in transform mode. | false |
| distanceFactor | Children are scaled by this factor and also by distance to a PerspectiveCamera, or zoom when using an OrthographicCamera. | |
| zIndexRange | Defines the Z-order range. | [16777271, 0] |
| portal | Reference to a target container (for rendering into a different DOM node). container. | |
| transform | If true, applies matrix3d transformations — the element appears as if it is inside the 3D scene. | false |
| sprite | Renders as a sprite. ➡️ Only in transform mode. | false |
| calculatePosition | Callback function to override the default positioning logic. Type: (object: Object3D, camera: Camera, size: { width: number; height: number }) => [number, number, number] Receives the related 3D object, the active camera, and the current viewport size, and must return [x, y, z] pixel coordinates for placing the HTML element. ➡️ Ignored in transform mode. | Default calculatePosition |
| occlude | Enables occlusion. Possible values: - true → Occlusion against all scene objects - Ref<TresObject3D>[] → Occlusion is enabled only against the specified objects. - 'blending' → Uses a blending-based occlusion method (CSS-like depth blending). | |
| geometry | Custom geometry to be used. | PlaneGeometry |
| material | Custom shader material used for the occlusion mesh. Only applies when occlude="blending" is enabled (an occlusion mesh is created). Ignored in raycast occlusion modes ( true, object refs). | |
| transparentMaterial | Enables transparent rendering for the occlusion material. Only applies when occlude="blending" creates an occlusion mesh. Ignored in raycast occlusion modes ( true, object refs). | false |
Events
| Event | Description |
|---|---|
| onOcclude | Called when the occlusion state changes. |
Exposed properties
| Property | Type | Description |
|---|---|---|
| instance | Ref<TresObject3D | null> | Reference to the root <TresGroup> used by <Html>. |
| isVisible | Ref<boolean> | Reactive value that indicates whether the HTML content is currently visible or occluded. |
| occlusionMesh | Ref<TresObject3D | null> | Reference to the occlusion mesh created when occlude="blending" is enabled. Used internally for geometry-based occlusion. |
Caveats
✨ When using
<Html occlude>, if the<Html>component is overlapping or inside a 3D object, it will be considered occluded and therefore hidden. To avoid this, adjust the position of the<Html>component in your scene.🎨 When using
<Html occlude="blending">, the HTML content is no longer selectable because it is rendered behind the canvas. This is required to achieve the blending effect.⚙️ When using a custom material with occlusion in
blendingmode, there are a few important requirements to ensure the HTML content renders correctly ⬇️See more information
- If you provide your own material, it must be transparent (
transparent: true) with an opacity < 1. - If you are not providing a custom material, enable
transparentMaterialso the internal shader becomes transparent. - The occlusion mesh requires a fully transparent canvas background; otherwise, thin borders or halo artifacts may appear.
- To compensate for the transparent canvas, you may reapply your previous clear-color as a CSS background on the
html,body, or a wrapperdiv.
- If you provide your own material, it must be transparent (
🔶 When using
transparentMaterial, overlapping<Html>elements (especially multipleocclude="blending"instances) may cause z-index or depth-order artifacts.
This happens because the occlusion mesh uses transparency in the WebGL layer while the DOM element uses CSS stacking order.🔵 To avoid thin border artifacts when using
occlude="blending", make sure your<TresCanvas>is fully transparent:vue<TresCanvas :alpha="true" :clearAlpha="0" />