---
title: Svelte
description: Paint scenes with Svelte 5 through @motionactor/renderers/svelte and @motionactor/dom/svelte.
---

Scenes, actors and effects are the same under every binding. A Svelte app changes two
things: its renderers are snippets or components, and it mounts through the `/svelte`
entries.

| Package | Svelte entry |
| --- | --- |
| `@motionactor/renderers` | `/svelte`: `SceneRenderer`, renderers, contexts, compositions, transitions, media |
| `@motionactor/dom` | `/svelte`: `dom` (the backend), `DomHost`, `ActorElement`, effect stages |
| `@motionactor/media` | `/svelte`: `ImageView`, `VideoView`, `AudioView`, `mediaRendererRegistry`, `withMediaFamily` |
| `@motionactor/engine` | `/svelte`: `mountSvelteComposition` |
| `@motionactor/hyperframes` | `/svelte`: `mountHyperframesComposition` |

The `/svelte` entries ship `.svelte` sources, so the app's own Svelte 5 compiler builds them
(`@sveltejs/vite-plugin-svelte` under Vite). A program loads one binding: importing
`/svelte` makes every `RenderNode` a Svelte node.

## Renderers

A renderer draws one actor class. A snippet is the usual form, since a snippet holds no
state and the registry calls renderers as pure functions. Export them from a module script:

```svelte title="views.svelte"
<script module lang="ts">
  import type {
    ActorRendererContextForBlueprint,
  } from "@motionactor/renderers/svelte";
  import type { Badge } from "./scene";

  export { badge };
</script>

{#snippet badge({ actor }: ActorRendererContextForBlueprint<typeof Badge>)}
  <span style:background="hsl({actor.signals.hue} 80% 45%)">
    {actor.signals.label}
  </span>
{/snippet}
```

An actor whose view needs state renders a component. It receives the same context as props
and may use runes, `useHold` and the other `use*` contexts:

```svelte title="PhotoView.svelte"
<script lang="ts">
  import {
    useHold,
    type ActorRendererContextForBlueprint,
  } from "@motionactor/renderers/svelte";
  import type { Photo } from "./scene";

  let { actor }: ActorRendererContextForBlueprint<typeof Photo> = $props();
  let image: HTMLImageElement | undefined = $state();
  let decoded: string | undefined = $state();
  const src = $derived(actor.signals.src);

  // Capture waits on this frame until the image decodes.
  useHold(() => `Photo: ${src}`, () => decoded !== src);
  $effect(() => {
    const element = image;
    const current = src;
    if (!element) return;
    let active = true;
    void element.decode().finally(() => {
      if (active) decoded = current;
    });
    return () => {
      active = false;
    };
  });
</script>

<img bind:this={image} {src} alt="" />
```

the backend's `snippet` and `component` register them:

```ts title="renderers.ts"
import { dom } from "@motionactor/dom/svelte";
import { createRenderRuntime } from "@motionactor/renderers/svelte";
import { Badge, Photo } from "./scene";
import PhotoView from "./PhotoView.svelte";
import { badge } from "./views.svelte";

export const runtime = createRenderRuntime({
  backends: [dom],
  renderers: [
    dom.snippet(Badge, badge),
    dom.component(Photo, PhotoView),
  ],
});
```

A host that places its children in the browser's flow paints each one through `Node`:

```svelte
{#snippet row({
  slots,
  renderChild,
}: ActorRendererContextForBlueprint<typeof Row>)}
  <div class="flex items-center gap-3">
    {#each slots.chips as { actor } (actor.id)}
      <Node node={renderChild(actor, { flow: {} })} />
    {/each}
  </div>
{/snippet}
```

The DOM backend measures a flow child while something reads its
[`measuredBox`](/docs/reactivity/channel#measured-boxes).

## Painting a scene

`provideRenderRuntime` sets the runtime for everything below it. `SceneRenderer` resolves
the frame and paints its layers:

```svelte title="Stage.svelte"
<script lang="ts">
  import type { CompiledScene } from "@motionactor/core";
  import {
    SceneRenderer,
    provideRenderRuntime,
  } from "@motionactor/renderers/svelte";
  import { runtime } from "./renderers";

  let { compiled, frame }: {
    compiled: CompiledScene<unknown>;
    frame: number;
  } = $props();
  provideRenderRuntime(runtime);
</script>

<SceneRenderer {compiled} {frame} width={1280} height={720} />
```

`provideFrameSource` and `provideFrameGate` take the same `ExternalFrameSource` and
`PromiseFrameGate` as the other bindings. `useMotionactorFrame`, `useMotionactorViewport` and
the other readers return a `Current<T>`, whose `current` is reactive.

## Transitions

`Transition` takes the actor and a `children` snippet. Each `Shared` is tagged by name, so it
morphs between the actor's views. With the `Standings` actor from
[Transitions](/docs/rendering/transitions):

```svelte title="views.svelte"
{#snippet standings({
  actor,
}: ActorRendererContextForBlueprint<typeof Standings>)}
  <Transition {actor}>
    {#snippet children(table)}
      <ol>
        {#each table.signals.teams as team (team.name)}
          <Shared name={Standings.shared.row(team.name)}>
            {team.name} {team.points}
          </Shared>
        {/each}
      </ol>
    {/snippet}
  </Transition>
{/snippet}
```

## Effects

`domStyle` effects, overlays included, paint unchanged. Canvas effects such as `glShader`
need the browser's HTML-in-canvas; provide it once near the root:

```svelte
<script lang="ts">
  import {
    nativeHtmlCanvas,
    provideHtmlCanvas,
  } from "@motionactor/renderers/svelte";

  provideHtmlCanvas(nativeHtmlCanvas);
</script>
```

Where the browser lacks it, the content paints unchanged under an overlay naming the missing
browser APIs.

## Compositions and hosts

A composition's `render` returns a Svelte node; `component` wraps a component and its props:

```ts title="composition.ts"
import {
  component,
  defineComposition,
} from "@motionactor/renderers/svelte";
import { titleScene } from "./scene";
import { runtime } from "./renderers";
import Stage from "./Stage.svelte";

export const titleCard = defineComposition({
  id: "TitleCard",
  width: 1280,
  height: 720,
  fps: 30,
  runtime,
  scene: titleScene,
  render: ({ compiled, frame }) => component(Stage, { compiled, frame }),
});
```

`mountSvelteComposition` from `@motionactor/engine/svelte` is the native preview, with the
controller [Hosts](/docs/rendering/hosts) describes. `@motionactor/hyperframes/svelte` mounts it for
HyperFrames. Headless capture reads the binding from the composition:

```ts twoslash title="session.ts"
import { openHeadlessSession } from "@motionactor/engine/node";

const session = await openHeadlessSession({
  entry: {
    entry: "./src/composition.ts",
    export: "titleCard",
  },
});
const jpeg = await session.still(30);
await session.close();
```
