Skip to content

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

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

vue
<Html occlude>

If occlude, then <Html> will be hidden by any objects that pass in front of its position.

Demo code
vue
<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

vue
<Html occlude="[mesh]">
Demo code
vue
<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

vue
<Html occlude="[mesh1, mesh2, mesh3, ...]" />

OR

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

vue
<Html occlude="blending">

The demo below ⬇️ (left black example) shows a basic usage example.

Demo code
vue
<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 geometry prop 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 as border-radius using 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
vue
<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
vue
<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

PropDescriptionDefault
asWrapping HTML element.'div'
wrapperClassThe className of the wrapping element. element.
prependProjects content behind the canvas.false
centerAdds a transform: translate(-50%, -50%).
➡️ Ignored in transform mode.
false
fullscreenAligns to the upper-left corner and fills the screen.
➡️ Ignored in transform mode.
false
distanceFactorChildren are scaled by this factor and also by distance to a PerspectiveCamera, or zoom when using an OrthographicCamera.
zIndexRangeDefines the Z-order range.[16777271, 0]
portalReference to a target container (for rendering into a different DOM node). container.
transformIf true, applies matrix3d transformations — the element appears as if it is inside the 3D scene.false
spriteRenders as a sprite.
➡️ Only in transform mode.
false
calculatePositionCallback 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
occludeEnables 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).
geometryCustom geometry to be used.PlaneGeometry
materialCustom 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).
transparentMaterialEnables 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

EventDescription
onOccludeCalled when the occlusion state changes.

Exposed properties

PropertyTypeDescription
instanceRef<TresObject3D | null>Reference to the root <TresGroup> used by <Html>.
isVisibleRef<boolean>Reactive value that indicates whether the HTML content is currently visible or occluded.
occlusionMeshRef<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 blending mode, there are a few important requirements to ensure the HTML content renders correctly ⬇️

    See more information
    1. If you provide your own material, it must be transparent (transparent: true) with an opacity < 1.
    2. If you are not providing a custom material, enable transparentMaterial so the internal shader becomes transparent.
    3. The occlusion mesh requires a fully transparent canvas background; otherwise, thin borders or halo artifacts may appear.
    4. To compensate for the transparent canvas, you may reapply your previous clear-color as a CSS background on the html, body, or a wrapper div.
  • 🔶 When using transparentMaterial, overlapping <Html> elements (especially multiple occlude="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" />