threlte logo
Basics

App Structure

Threlte uses Svelte’s Context API to share renderer, camera, and others to any child component. Hooks like useThrelte() read from this context, so anything that needs it must be inside <Canvas>.

SomeComponent.svelte
<script>
  import { useThrelte } from '@threlte/core'

  const { camera, renderer } = useThrelte()
</script>

Wrap your 3D app in a single <Canvas> and give it one direct child: Scene.svelte. Put all 3D content under that node to access Threlte hooks.

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 />

Context not available

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>

Using a single <Canvas>

In a larger Svelte app or when using SvelteKit, prefer a single <Canvas> to avoid the WebGL error “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>