Skip to content

App Structure

Threlte makes heavy use of Svelte’s Context API as a way to pass data through the component tree without having to pass props down manually at every level:

SomeComponent.svelte
<script>
import { useThrelte } from '@threlte/core'
const { camera, renderer } = useThrelte()
</script>

To let Threlte do its magic, we recommend to follow our best practices for structuring your app.

The <Canvas> component provides all basic contexts in a Threlte application. The recommended app structure is to have a single child component of <Canvas> (typically named “Scene.svelte” in examples) for your Threlte app. This will allow contexts provided by useThrelte and other hooks to be used.

App.svelte
<script>
import { Canvas } from '@threlte/core'
import Scene from './Scene.svelte'
</script>
<Canvas>
<Scene />
</Canvas>
Scene.svelte
<script>
import { T, useTask } from '@threlte/core'
import { interactivity } from '@threlte/extras'
import Player from './Player.svelte'
import World from './World.svelte'
let rotation = $state(0)
// useTask is relying on a context provided
// by <Canvas>. Because we are definitely *inside*
// <Canvas>, we can safely use it.
useTask((delta) => {
rotation += delta
})
// This file is also typically the place to
// inject plugins
interactivity()
</script>
<T.Mesh rotation.y={rotation}>
<T.BoxGeometry />
<T.MeshBasicMaterial color="red" />
</T.Mesh>
<Player />
<World />

The following app structure is deceiving. It looks like it should work, but it will not. The problem is that the useTask hook is called outside of the <Canvas> component, so the main Threlte context is not available. Usually hooks relying on some context will tell you with descriptive error messages when they are used outside of their context.

App.svelte
<script>
import { Canvas, useTask, T } from '@threlte/core'
let rotation = 0
// This won't work, we're not inside <Canvas>
useTask((delta) => {
rotation += delta
})
</script>
<Canvas>
<T.Mesh rotation.y={rotation} />
</Canvas>

In a typical SvelteKit application, a single <Canvas> component instance renders all 3D content to circumvent the WebGL Error "WARNING: Too many active WebGL contexts. Oldest context will be lost.". We can use Svelte 5 Snippets to easily portal our 3D content to a global <Canvas>. For that, let’s first create a component that renders portal content:

src/lib/components/CanvasPortalTarget.svelte
<script
lang="ts"
module
>
import type { Snippet } from 'svelte'
import { SvelteSet } from 'svelte/reactivity'
let snippets = new SvelteSet<Snippet>()
export const addCanvasPortalSnippet = (snippet: Snippet) => {
snippets.add(snippet)
}
export const removeCanvasPortalSnippet = (snippet: Snippet) => {
snippets.delete(snippet)
}
</script>
{#each snippets as snippet}
{@render snippet()}
{/each}

Then implement this component in the root +layout.svelte component:

src/routes/+layout.svelte
<script lang="ts">
import CanvasPortalTarget from '$lib/components/CanvasPortalTarget.svelte'
import { Canvas } from '@threlte/core'
import type { Snippet } from 'svelte'
let { children }: { children: Snippet } = $props()
</script>
<div>
<Canvas>
<CanvasPortalTarget />
</Canvas>
</div>
{@render children()}
<style>
div {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>

All we need is another component that we can utilize to easily portal 3D content into the global canvas:

src/lib/components/CanvasPortal.svelte
<script lang="ts">
import {
addCanvasPortalSnippet,
removeCanvasPortalSnippet
} from '$lib/components/CanvasPortalTarget.svelte'
import { onDestroy, type Snippet } from 'svelte'
let { children }: { children: Snippet } = $props()
addCanvasPortalSnippet(children)
onDestroy(() => {
removeCanvasPortalSnippet(children)
})
</script>

Now we can use this component to portal 3D content into the global canvas from anywhere in our app all while using regular DOM elements alongside our 3D content:

src/routes/+page.svelte
<script lang="ts">
import CanvasPortal from '$lib/components/CanvasPortal.svelte'
import { T } from '@threlte/core'
</script>
<!-- Regular DOM elements for UI -->
<button>Click me</button>
<!-- 3D content -->
<CanvasPortal>
<T.PerspectiveCamera
position.z={10}
makeDefault
/>
<T.Mesh>
<T.BoxGeometry />
<T.MeshBasicMaterial color="red" />
</T.Mesh>
</CanvasPortal>