Copy-paste starting points for Threlte 8 in v10r. Threlte only — no vanilla Three.js. For the architecture and project decisions, read docs/blueprint/3d/3d-integration.md; for Three.js / Threlte fundamentals, use the 3d skill.
Dependencies
// package.json — then rebuild/restart the container
"@threlte/core": "^8.3.1",
"@threlte/extras": "^9.0.0",
"three": "^0.183.2"
Page Setup
Disable SSR and prerender once at the 3d/ layout — WebGL needs window, and the Three.js bundle is browser-only:
// showcases/3d/+layout.ts
export const ssr = false;
export const prerender = false;
A leaf that breaks out of the layout ([model]/+page@.svelte) repeats these flags in its own +page.ts.
Canvas + Scene
Page hosts <Canvas> and wraps it in <svelte:boundary> for the WebGL fallback. Scene contents live in a child component for code-splitting.
<!-- +page.svelte -->
<script lang="ts">
import { Canvas } from '@threlte/core';
import { BoundaryFallback } from '$lib/components/composites';
import Scene from './Scene.svelte';
</script>
<svelte:boundary>
<div class="container">
<Canvas>
<Scene />
</Canvas>
</div>
{#snippet failed(error, reset)}
<BoundaryFallback
title="3D scene unavailable"
description="WebGL is required. Check browser support or graphics drivers."
minHeight="100vh"
{reset}
/>
{/snippet}
</svelte:boundary>
<style>
.container { width: 100%; height: 100vh; }
</style>
Scene Contents
Scene is <T> proxy components — camera, lights, meshes. No new THREE.Scene(), no renderer, no render loop; Threlte owns the renderer lifecycle.
<!-- Scene.svelte -->
<script lang="ts">
import { T } from '@threlte/core';
import { OrbitControls } from '@threlte/extras';
</script>
<T.PerspectiveCamera makeDefault position={[3, 3, 3]}>
<OrbitControls />
</T.PerspectiveCamera>
<T.DirectionalLight position={[10, 10, 10]} intensity={1} />
<T.AmbientLight intensity={0.5} />
<T.Mesh>
<T.BoxGeometry />
<T.MeshStandardMaterial color="hotpink" />
</T.Mesh>
Animation: useTask
Per-frame work uses useTask from @threlte/core (Threlte 8 — not useFrame, that's Threlte 7). Drive reactive props with $state:
<script lang="ts">
import { T, useTask } from '@threlte/core';
let rotation = $state(0);
useTask((delta) => {
rotation += delta;
});
</script>
<T.Mesh rotation.y={rotation}>
<T.BoxGeometry />
<T.MeshStandardMaterial color="hotpink" />
</T.Mesh>
Reactive Props
Props from $props() flow straight into <T> attributes — no manual needsUpdate. Use $effect only for imperative objects Threlte doesn't proxy (e.g. an AnimationMixer):
<script lang="ts">
let { color = '#ff0000', wireframe = false } = $props();
</script>
<T.Mesh>
<T.BoxGeometry />
<T.MeshStandardMaterial {color} {wireframe} />
</T.Mesh>
GLTF Loading
Load with useGltf (returns a store — read $gltf) or the <GLTF> component. Guard the render until it resolves:
<script lang="ts">
import { T, useTask } from '@threlte/core';
import { useGltf } from '@threlte/extras';
import { AnimationMixer } from 'three';
let { defaultAnimation }: { defaultAnimation?: string } = $props();
// svelte-ignore state_referenced_locally
const gltf = useGltf('/models/Fox.glb');
let mixer: AnimationMixer | undefined;
$effect(() => {
const data = $gltf;
if (!data || !defaultAnimation) return;
mixer = new AnimationMixer(data.scene);
const clip = data.animations.find((c) => c.name === defaultAnimation);
if (clip) mixer.clipAction(clip).play();
});
useTask((delta) => mixer?.update(delta));
</script>
{#if $gltf}
<T is={$gltf.scene} />
{/if}
GLB models ship from static/models/ and serve at /models/*.glb. The <GLTF url="..." /> component is the no-animation shortcut.
Route Structure
3D showcases live under the public locale group (routes are /showcases/3d/..., plural):
src/routes/[[locale=locale]]/(public)/showcases/3d/
├── +layout.ts # ssr=false, prerender=false (whole subtree)
├── +page.svelte # scene gallery
├── static-scene/ # GLTF helmet, OrbitControls
├── animated-scene/
│ ├── +page.svelte # Canvas + boundary
│ └── Scene.svelte # split for code-splitting
├── customize/[model]/ # GLTF customizer
└── [model]/ # config-driven viewer
Gotchas
useTask, notuseFrame— Threlte 8 renamed the per-frame hook.useGltfis a store — read$gltf, never render scene contents before it resolves.makeDefaulttype error —T.PerspectiveCamera makeDefaultneeds@ts-ignoreuntil tsgo supports the conditional type.state_referenced_locally— callinguseGltf(prop)at script top reads a prop during init; silence with// svelte-ignore state_referenced_locally.- Keep 3D imports per route — don't pull Three.js into a shared
+layout; that defeats code-splitting.
References
docs/blueprint/3d/3d-integration.md— architecture, components, config registry3dskill — Three.js + Threlte fundamentals, physics, WebGPU/showcases/3d— live scenes (also the test spec)- Threlte: https://threlte.xyz/docs