<HTML>
This component is a port of drei’s <Html>
component. It allows you to tie HTML
content to any object of your scene. It will be projected to the objects
whereabouts automatically.
The container of your <Canvas> component needs to be set to position: relative | absolute | sticky | fixed. This is because the DOM element will
be mounted as a sibling to the <canvas> element.
<script lang="ts"> import Scene from './Scene.svelte' import { Canvas } from '@threlte/core' import { Checkbox, Pane } from 'svelte-tweakpane-ui'
let autoRender = $state(true)</script>
<Pane position="fixed"> <Checkbox label="auto render" bind:value={autoRender} /></Pane>
<div> <Canvas> <Scene {autoRender} /> </Canvas></div>
<style> div { height: 100%; }</style><script lang="ts"> import { MathUtils } from 'three' import { T } from '@threlte/core' import { HTML, OrbitControls } from '@threlte/extras' import { Spring } from 'svelte/motion'
type Props = { autoRender?: boolean }
let { autoRender = true }: Props = $props()
const getRandomColor = () => `#${Math.floor(Math.random() * 16777215) .toString(16) .padStart(6, '0')}`
let color = $state(getRandomColor()) let isHovering = $state(false) let isPointerDown = $state(false)
let htmlPosZ = new Spring(0) $effect(() => { htmlPosZ.set(isPointerDown ? -0.15 : isHovering ? -0.075 : 0, { hard: isPointerDown }) })</script>
<T.PerspectiveCamera position={[10, 5, 10]} makeDefault fov={30} oncreate={(ref) => ref.lookAt(0, 0.75, 0)}> <OrbitControls target.y={0.75} maxPolarAngle={85 * MathUtils.DEG2RAD} minPolarAngle={20 * MathUtils.DEG2RAD} maxAzimuthAngle={45 * MathUtils.DEG2RAD} minAzimuthAngle={-45 * MathUtils.DEG2RAD} enableZoom={false} /></T.PerspectiveCamera>
<T.DirectionalLight position={[0, 10, 10]} />
<T.AmbientLight intensity={0.3} />
<T.GridHelper />
<T.Mesh position.y={0.5}> <T.MeshStandardMaterial {color} /> <T.SphereGeometry args={[0.5]} /> <HTML position.y={1.25} position.z={htmlPosZ.current} transform {autoRender} > <button onpointerenter={() => (isHovering = true)} onpointerleave={() => { isPointerDown = false isHovering = false }} onpointerdown={() => { isPointerDown = true color = getRandomColor() }} onpointerup={() => (isPointerDown = false)} onpointercancel={() => { isPointerDown = false isHovering = false }} class="rounded-full bg-orange-500 px-3 text-white hover:opacity-90 active:opacity-70" > I'm a regular HTML button </button> </HTML>
<HTML position.x={0.75} transform pointerEvents="none" {autoRender} > <p class="w-auto translate-x-1/2 text-xs drop-shadow-lg" style="color: {color}" > color: {color} </p> </HTML></T.Mesh>Basic example
Section titled “Basic example”<script lang="ts"> import { HTML } from '@threlte/extras'</script>
<HTML> <h1>Hello, World!</h1></HTML>Transform
Section titled “Transform”transform applies matrix3d transformations.
<script lang="ts"> import { HTML } from '@threlte/extras'</script>
<HTML transform> <h1>Hello World</h1></HTML>Occlude
Section titled “Occlude”<Html> can be occluded behind geometry using the occlude occlude property.
<script lang="ts"> import { HTML } from '@threlte/extras'</script>
<HTML transform occlude> <h1>Hello World</h1></HTML>Setting occlude to "blending" will allow objects to partially occlude the
<HTML> component.
This occlusion mode requires the <canvas> element to have pointer-events
set to none. Therefore, any events like those in OrbitControls must be
set on the canvas parent. Extras components like <OrbitControls> do this
automatically.
<script lang="ts"> import { Canvas } from '@threlte/core' import Scene from './Scene.svelte'</script>
<div> <Canvas> <Scene /> </Canvas></div>
<style> div { height: 100%; }</style><script lang="ts"> import { MeshStandardMaterial, TetrahedronGeometry, CylinderGeometry, ConeGeometry, SphereGeometry, IcosahedronGeometry, TorusGeometry, OctahedronGeometry, BoxGeometry, MathUtils } from 'three' import { T } from '@threlte/core' import { Float } from '@threlte/extras'
const material = new MeshStandardMaterial() const geometries = [ { geometry: new TetrahedronGeometry(2) }, { geometry: new CylinderGeometry(0.8, 0.8, 2, 32) }, { geometry: new ConeGeometry(1.1, 1.7, 32) }, { geometry: new SphereGeometry(1.5, 32, 32) }, { geometry: new IcosahedronGeometry(2) }, { geometry: new TorusGeometry(1.1, 0.35, 16, 32) }, { geometry: new OctahedronGeometry(2) }, { geometry: new SphereGeometry(1.5, 32, 32) }, { geometry: new BoxGeometry(2.5, 2.5, 2.5) } ] as const
const n = 40 const randProps = Array.from( { length: n }, () => geometries[Math.floor(Math.random() * geometries.length)] )</script>
{#each randProps as prop} <Float floatIntensity={0} rotationIntensity={2} rotationSpeed={2} > <T.Mesh scale={MathUtils.randFloat(0.25, 0.5)} position={[ MathUtils.randFloat(-8, 8), MathUtils.randFloat(-8, 8), MathUtils.randFloat(-8, 8) ]} geometry={prop?.geometry} {material} /> </Float>{/each}// From: https://discourse.threejs.org/t/roundedrectangle-squircle/28645/20import { BufferGeometry, BufferAttribute } from 'three'
export class RoundedPlaneGeometry extends BufferGeometry { parameters: { width: number height: number radius: number segments: number } constructor(width = 1, height = 1, radius = 0.2, segments = 16) { super() this.parameters = { width, height, radius, segments }
// helper consts const wi = width / 2 - radius // inner width const hi = height / 2 - radius // inner height const ul = radius / width // u left const ur = (width - radius) / width // u right const vl = radius / height // v low const vh = (height - radius) / height // v high
let positions = [wi, hi, 0, -wi, hi, 0, -wi, -hi, 0, wi, -hi, 0] let uvs = [ur, vh, ul, vh, ul, vl, ur, vl]
let n = [ 3 * (segments + 1) + 3, 3 * (segments + 1) + 4, segments + 4, segments + 5, 2 * (segments + 1) + 4, 2, 1, 2 * (segments + 1) + 3, 3, 4 * (segments + 1) + 3, 4, 0 ] as const
const indices: number[] = [ n[0], n[1], n[2], n[0], n[2], n[3], n[4], n[5], n[6], n[4], n[6], n[7], n[8], n[9], n[10], n[8], n[10], n[11] ] let phi, cos, sin, xc, yc, uc, vc, idx
for (let i = 0; i < 4; i++) { xc = i < 1 || i > 2 ? wi : -wi yc = i < 2 ? hi : -hi uc = i < 1 || i > 2 ? ur : ul vc = i < 2 ? vh : vl for (let j = 0; j <= segments; j++) { phi = (Math.PI / 2) * (i + j / segments) cos = Math.cos(phi) sin = Math.sin(phi) positions.push(xc + radius * cos, yc + radius * sin, 0) uvs.push(uc + ul * cos, vc + vl * sin) if (j < segments) { idx = (segments + 1) * i + j + 4 indices.push(i, idx, idx + 1) } } }
this.setIndex(new BufferAttribute(new Uint32Array(indices), 1)) this.setAttribute('position', new BufferAttribute(new Float32Array(positions), 3)) this.setAttribute('uv', new BufferAttribute(new Float32Array(uvs), 2)) }}<script lang="ts"> import { T } from '@threlte/core' import { Environment, Float, HTML, useGltf, OrbitControls } from '@threlte/extras' import { type Mesh, MathUtils } from 'three' import Geometries from './Geometries.svelte' import { RoundedPlaneGeometry } from './RoundedPlaneGeometry'
const gltf = useGltf<{ nodes: { phone: Mesh } materials: {} }>('/models/phone/phone.glb')
const phoneGeometry = $derived($gltf?.nodes.phone.geometry)
const url = window.origin</script>
<T.PerspectiveCamera position={[50, -30, 30]} fov={20} oncreate={(ref) => { ref.lookAt(0, 0, 0) }} makeDefault> <OrbitControls enableDamping enableZoom={false} /></T.PerspectiveCamera>
<T.AmbientLight intensity={0.3} />
<Environment url="/textures/equirectangular/hdr/shanghai_riverside_1k.hdr" />
<Float scale={0.7} floatIntensity={5}> <HTML rotation.y={90 * MathUtils.DEG2RAD} position.x={1.2} transform occlude="blending" geometry={new RoundedPlaneGeometry(10.5, 21.3, 1.6)} > <div class="phone-wrapper" style="border-radius:1rem" > <iframe title="" src={url} width="100%" height="100%" frameborder="0" ></iframe> </div> </HTML>
{#if phoneGeometry} <T.Mesh scale={5.65} geometry={phoneGeometry} > <T.MeshStandardMaterial color="#FF3F00" metalness={0.9} roughness={0.1} /> </T.Mesh> {/if}</Float>
<Geometries />
<style> .phone-wrapper { height: 848px; width: 420px; border-radius: 63px; overflow: hidden; border: 1px solid rgba(0, 0, 0, 0.1); }</style>Visibility change event
Section titled “Visibility change event”Use the property occlude and bind to the event visibilitychange to
implement a custom hide/show behaviour.
<script lang="ts"> import { HTML } from '@threlte/extras'
const onVisibilityChange = (isVisible: boolean) => { console.log(isVisible) }</script>
<HTML transform occlude onvisibilitychange={onVisibilityChange}> <h1>Hello World</h1></HTML>When binding to the event visibilitychange the contents of <HTML> is
not automatically hidden when it’s occluded.
Sprite rendering
Section titled “Sprite rendering”Use the property sprite in transform mode to render the contents of
<HTML> as a sprite.
<script lang="ts"> import { HTML } from '@threlte/extras'</script>
<HTML transform sprite> <h1>Hello World</h1></HTML>Center
Section titled “Center”Add a -50%/-50% css transform with center when not in transform mode.
<script lang="ts"> import { HTML } from '@threlte/extras'</script>
<HTML center> <h1>Hello World</h1></HTML>Portal
Section titled “Portal”Use the property portal to mount the contents of the <HTML> component on
another HTMLElement. By default the contents are mounted as a sibling to the
rendering <canvas>.
<script lang="ts"> import { HTML } from '@threlte/extras'</script>
<HTML portal={document.body}> <h1>Hello World</h1></HTML>Pausing the render task
Section titled “Pausing the render task”<HTML> has an autoRender prop that you can use to pause its
render task. If at some point you no longer need to update <HTML>,
you can set autoRender to false. If you need to resume updates,
set autoRender back to true.
<HTML> also exports its internal render task and the startRendering,
stopRendering, and render functions so you can either manually render
or start and stop the internal task based on your needs.
<script lang="ts"> import { HTML } from '@threlte/extras'
let html = $state<HTML>()
$effect(() => { html?.render() })</script>
<HTML autoRender={false} bind:this={html}> <h1>Hello World</h1></HTML><script lang="ts"> import { HTML } from '@threlte/extras'
let html = $state<HTML>()
// turn this on and off in accordance with your application let renderWhileTrue = $state(false)
$effect(() => { if (renderWhileTrue) { html?.startRendering() // always stop rendering if it was started return () => { html?.stopRendering() } } })</script>
<HTML autoRender={false} bind:this={html}> <h1>Hello World</h1></HTML>In both cases you should set autoRender to false so that the render task
doesn’t automatically begin.
Lastly, you can access these functions from the <HTML>’s children snippet.
<HTML autoRender={false}> {#snippet children({ render, startRendering, stopRendering })} <button onclick={startRendering}>start rendering</button> <button onclick={stopRendering}>stop rendering</button> <button onclick={render}>render a single frame</button> {/snippet}</HTML>An alternative to using HTML for UI is
uikit. The
vanilla code has be wrapped into
threlte-uikit for use in
threlte projects. There are situations where this package is necessary, for
instance the <HTML/> component cannot be used within XR sessions.