---
title: Renderers
description: A renderer draws one actor class on one backend from the resolved state of one frame.
---

A renderer is a function from one resolved actor to what a backend draws. It declares the
backend it draws for and the classes it handles, and it gets everything else from the frame.

```tsx twoslash title="badge.tsx"
// @jsx: react-jsx
// ---cut---
import { LayoutActor, signal } from "@motionactor/core";
import { dom } from "@motionactor/dom/react";

export class Badge extends LayoutActor {
  label = signal("New");
  hue = signal(190);
}

export const badge = dom.render(Badge, ({ actor }) => (
  <span style={{ background: `hsl(${actor.signals.hue} 80% 45%)` }}>
    {actor.signals.label}
  </span>
));
```

`actor.signals` is typed from the class, so `hue` is a number and `label` a string here. The
renderer never sees a signal object, only that signal's value at this frame.

A renderer that reads `actor` draws again whenever anything about the actor changes. One that
reads `signals` instead draws again only when a key it read changes, so moving, scaling or
fading the badge leaves it alone:

```tsx twoslash title="badge.tsx"
// @jsx: react-jsx
// ---cut---
import { LayoutActor, signal } from "@motionactor/core";
import { dom } from "@motionactor/dom/react";

export class Badge extends LayoutActor {
  label = signal("New");
  hue = signal(190);
}

export const badge = dom.render(Badge, ({ signals }) => (
  <span style={{ background: `hsl(${signals.hue} 80% 45%)` }}>{signals.label}</span>
));
```

The keys are recorded wherever they are read: destructured, in a memoized component or an effect
handed `signals`. A spread reads every key. The React binding tracks keys; Vue and Svelte update
the content on any change to the actor.

## Matching

`backend.render(classes, render, options?)` takes one class or an array of them. A renderer matches an actor whose class is one of those classes or extends one, so a
renderer for `Badge` also draws `class Pill extends Badge` until something more specific
claims it.

When several renderers match, the first one wins. A render runtime orders them as listed in
`renderers`, then each backend's own primitives, so an app renderer for a subclass always
comes before the backend's renderer for its base. A renderer that returns `null` passes the
actor on to the next match.

```tsx twoslash title="runtime.tsx"
// @jsx: react-jsx
// @filename: badge.tsx
import { LayoutActor, signal } from "@motionactor/core";
import { dom } from "@motionactor/dom/react";
export class Badge extends LayoutActor {
  label = signal("New");
  hue = signal(190);
}
export const badge = dom.render(Badge, ({ actor }) => <span>{actor.signals.label}</span>);
// @filename: runtime.tsx
// ---cut---
import { signal } from "@motionactor/core";
import { createRenderRuntime, createRendererRegistry } from "@motionactor/renderers/react";
import { dom } from "@motionactor/dom/react";
import { Badge, badge } from "./badge";

class Pill extends Badge {
  muted = signal(false);
}

// Draws muted pills and passes the rest on to `badge`.
const mutedPill = dom.render(Pill, ({ actor }) =>
  actor.signals.muted ? (
    <span className="pill">{actor.signals.label}</span>
  ) : null,
);

export const badges = createRendererRegistry(mutedPill, badge);

export const runtime = createRenderRuntime({
  backends: [dom],
  renderers: [badges],
});
```

A registry is a reusable, ordered group of renderers. Registries nest, and
`createRendererRegistry(base, { overrides: [...] })` puts the overrides ahead of everything in
`base`, the last override first. Pass a registry wherever a renderer list is expected.

An actor with renderers on several backends gets the one for the layer it is in. The runtime
routes each renderer to the backend it names and throws when it lists a renderer for a backend
it does not have.

## The render context

| Field | What it holds |
| --- | --- |
| `actor` | The resolved actor: `signals`, `localLayout`, `worldLayout`, `effects`, `compositing`, `channels` |
| `state` | The whole `FrameState`, for reading other actors with `state.findActor(Class)` |
| `frame`, `fps` | The frame being drawn and the build's frame rate |
| `compiled` | The compiled scene, when the host has one |
| `slots` | Hosted children by slot name, each as `{ actor, element }`; a `multiple` slot is an array. Painting an `element` does not read its child; reading `actor` makes the renderer draw again when that child changes |
| `targets` | The actor's declared effect targets, the same identities scene code applies effects to |
| `refs` | DOM: ref callbacks for those targets, so an effect can find the element, and the [bindings](#bindings) `refs.<child>` and `refs.self`. Three: the [bindings](#three-bindings) alone |
| `renderChild` | Draws a child actor through the same registry: a slot entry's `actor`, or a copy derived from it. DOM: `renderChild(actor, { flow })` places it in the element's flow instead of at its layout box |

## Slots

A slot is a place in an actor that other actors are spawned into. The renderer decides where
each child's element goes; the child's own renderer draws the child.

```tsx twoslash title="card.tsx"
// @jsx: react-jsx
// ---cut---
import { createScene, LayoutActor, signal, slot } from "@motionactor/core";
import { dom } from "@motionactor/dom/react";

class Tag extends LayoutActor {
  text = signal("");
}

class Card extends LayoutActor {
  title = signal("Untitled");
  tags = slot({ accepts: Tag, multiple: true });
}

export const compiled = createScene(function* (ctx) {
  const card = ctx.spawn(Card, { title: "Release notes" });
  card.tags.spawn(Tag, { text: "2.0" });
  card.tags.spawn(Tag, { text: "breaking" });
  return { card };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });

export const card = dom.render(Card, ({ actor, slots }) => (
  <article>
    <h2>{actor.signals.title}</h2>
    <ul>
      {slots.tags.map(({ actor, element }) => (
        <li key={actor.id}>{element}</li>
      ))}
    </ul>
  </article>
));
```

## Bindings

A DOM renderer can paint a `child(...)` actor onto a node of its own markup instead of the
child's element: `refs.<child>` binds the node to that child, and `refs.self` binds it to the
actor itself. The node keeps its place in the markup's flow, and each frame the scope writes
the actor's style onto it with no render of the view.

```tsx twoslash title="card.tsx"
// @jsx: react-jsx
// ---cut---
import { child, createScene, LayoutActor, signal } from "@motionactor/core";
import { dom } from "@motionactor/dom/react";

class Title extends LayoutActor {}

class Card extends LayoutActor {
  label = signal("Quarterly");
  title = child(Title);
}

export const compiled = createScene(function* (ctx) {
  const card = ctx.spawn(Card);
  yield* card.title.transform.scale.tween(1.2, 0.4);
  return { card };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });

export const card = dom.render(Card, ({ signals, refs }) => (
  <article>
    <h1 ref={refs.title}>{signals.label}</h1>
  </article>
));
```

Vue binds with the `v-actor` directive from `@motionactor/dom/vue`
(`<h1 v-actor="refs.title">`), and Svelte with an attachment (`<h1 {@attach refs.title}>`).

| While bound | The node gets |
| --- | --- |
| The actor is present | `transform: scale() rotate()` about the node's own origin, `opacity`, blend mode, isolation and clip, and `shape.radius` as `border-radius` with `overflow: hidden` |
| It gets an inline effect | What the effect's stage paints on an element's wrapper, folded in: its `filter`, its `transform` after the actor's own, its `opacity` multiplied, its `clip-path`. The effect's box is the node's own untransformed size, measured once and again when the node resizes |
| The actor is absent | `visibility: hidden`, so it keeps its space |
| The node unbinds | Markup's own inline values for those properties back, as they were when it bound |

The markup places and sizes the node: the actor's `x`, `y`, width and height are not written.
Whatever writes one of those properties into the node's `style` (the view re-rendering, a
component's own state, a script), the node takes the actor's value back before paint. Its
`measuredBox` is published on demand, untransformed, against the element of the actor whose
view holds it, so a rotated node measures the box it was laid out in. An outlet rendered inside
a bound node is placed against that node, where flow put it, and follows it when flow moves it:
on every frame the scope presents, held or repeated, when the node resizes, and when fonts
load. Each node bound is its own instance: a view that binds a child and also renders its
outlet paints it twice.

Only HTML elements bind: `refs.<child>` and `refs.self` take `HTMLElement | null`. A binding
throws a `BoundElementError` at bind, in every build, naming the actor and the field, when the actor's content is transformed
(`contentTransform`), when it gets an offscreen effect that does not clip or that no actor
element encloses, an effect whose stage paints only actor elements, when an effect needs an
overlay or a second clip, or for `refs.self` inside the actor's own element, which already
paints it; a refused node registers nothing, so the view has nothing to release. An effect the
binding could not see at bind (an overlay a later frame starts, or any effect when the render
context carries no compiled scene) throws as the frame that brings it writes the node: the
binding lets go of the node first, back to its markup, and the frame's other nodes still paint.
A target whose name a binding also uses is a type error on `refs`.

### Offscreen effects

A bound node takes a `glShader` or `canvasPaint` effect when the effect's target contract
clips (`clips: true`): its handler paints inside the target's rect and leaves the rest of the
canvas as it was. The node registers in the capture of the actor element whose view binds it,
and that element mounts its canvas for the effect and runs it over the node's rect. An effect
without `target` paints its whole canvas, the element around the node, so binding a node to an
actor that gets one throws a `BoundElementError`, in every build, before the node registers.

```tsx twoslash title="frosted-card.tsx"
// @jsx: react-jsx
// ---cut---
import { child, createScene, defineEffect, effectTargetContract, LayoutActor, registerEffects, signal } from "@motionactor/core";
import { glShader, targetRegionUniforms, type CanvasTargetContract } from "@motionactor/dom";
import { dom } from "@motionactor/dom/react";

const FROST_FS = `#version 300 es
precision highp float;
uniform sampler2D u_tex;
uniform bool u_hasRegion;
uniform vec4 u_regionRect;
uniform float u_amount;
in vec2 v_uv;
out vec4 color;
void main() {
  vec4 source = texture(u_tex, v_uv);
  vec2 local = (vec2(v_uv.x, 1.0 - v_uv.y) - u_regionRect.xy) / u_regionRect.zw;
  bool inside = all(greaterThanEqual(local, vec2(0.0))) && all(lessThanEqual(local, vec2(1.0)));
  color = u_hasRegion && !inside ? source : mix(source, vec4(1.0), u_amount);
}`;

/** Any rect it clips to: a target, or the node a view binds. */
type ClippedRect = CanvasTargetContract<unknown, "rect", true>;

export const frost = defineEffect(glShader, {
  name: "frost",
  params: { amount: signal(0.4) },
  target: effectTargetContract<ClippedRect>({ dataKinds: [], geometry: ["rect"], clips: true }),
  shader: { fs: FROST_FS },
  uniforms: ({ amount }, { target }) => ({ ...targetRegionUniforms(target), amount }),
});

declare module "@motionactor/core" {
  interface ActorEffects {
    "dom:frost": typeof frost;
  }
}
registerEffects([frost]);

class Card extends LayoutActor {
  panel = child(LayoutActor);
}

export const compiled = createScene(function* (ctx) {
  const card = ctx.spawn(Card);
  card.panel.fx.frost();
  yield* ctx.wait(1);
  return { card };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });

export const card = dom.render(Card, ({ refs }) => (
  <article>
    <div ref={refs.panel}>Q3 revenue</div>
  </article>
));
```

`card.panel.fx` offers every installed effect on any child: whether the view binds it or
renders it through its outlet is the view's choice, so the check runs at bind. A child the view renders through its
outlet keeps its own element, which runs any of its effects in a canvas of its own. A page
binding has no actor element around it, so an offscreen effect on its actor throws at bind.
While a bound node has no area (`display: none`, zero size), its effect paints nothing; it
stays registered and paints again once the node shows.

### Three bindings

A three renderer binds the same way, to objects of its own markup: `refs.self` binds an
Object3D to the actor, and `refs.<child>` binds one to each `child(...)`. Any actor binds. At the
start of each advance of the canvas, before the composer and the render, the host writes each of
`position`, `rotation` and `scale` the actor's blueprint declares as an x/y/z group onto the
object, so a frame that only moves an actor renders no component. An `Object3DActor` declares all
three; a `LayoutActor` declares none, so its object keeps the placement its markup gives it.

```tsx twoslash title="rig.tsx"
// @jsx: react-jsx
// ---cut---
import { child, createScene } from "@motionactor/core";
import { Object3DActor, Primitive3D } from "@motionactor/three";
import { three } from "@motionactor/three/react";

class Rig extends Object3DActor {
  lid = child(Primitive3D);
}

export const compiled = createScene(function* (ctx) {
  const rig = ctx.spawn(Rig);
  yield* rig.lid.rotateTo({ x: 0, y: Math.PI, z: 0 }, 1);
  return { rig };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });

export const rig = three.render(Rig, ({ refs }) => (
  <group ref={refs.self}>
    <mesh ref={refs.lid}>
      <boxGeometry />
      <meshStandardMaterial />
    </mesh>
  </group>
));
```

| While bound | The object gets |
| --- | --- |
| The actor is present | Each of `position`, `rotation` and `scale` its blueprint declares |
| The actor is absent | `visible = false` |
| The object unbinds | Its visibility, and each group it took, as it had them when it bound |

A bound object is the actor's effect target, whatever the actor: `rig.lid.fx.<name>()` selects
the meshes under the object bound to `lid`. A LayoutActor drawn by a three view binds its root
the same way, and takes effects without any placement:

```tsx twoslash title="gallery.tsx"
// @jsx: react-jsx
// ---cut---
import { LayoutActor } from "@motionactor/core";
import { three } from "@motionactor/three/react";

class Gallery extends LayoutActor {}

export const gallery = three.render(Gallery, ({ refs }) => (
  <group ref={refs.self} position={[0, 0, -4]}>
    <mesh>
      <planeGeometry />
      <meshStandardMaterial />
    </mesh>
  </group>
));
```

Only Object3Ds bind; anything else throws a `BoundObjectError`. `Primitive3D`'s own renderer
binds its mesh through `refs.self`.

## Page bindings

Markup outside every actor's view binds an actor too. `actor.<tag>` from
`@motionactor/dom/react` renders that element bound to the actor a live handle names: a handle
from the scene's exports, `compiled.scene`. Render it among `SceneRenderer`'s children, which
render in the scope of the scene's DOM layer. Below, nothing draws the card: the heading is
the page's own, and each frame the DOM layer writes the title's style onto it with no render
of the page.

<PageHeadingScene />

```tsx twoslash title="page-heading.render.tsx"
// @jsx: react-jsx
// @filename: page-heading.scene.ts
import { child, createScene, Easing, LayoutActor } from "@motionactor/core";

export class Title extends LayoutActor {}

export class Card extends LayoutActor {
  title = child(Title);
}

export const scene = createScene(function* (ctx) {
  yield* ctx.wait(0.4);
  const card = ctx.spawn(Card);

  // Nothing draws the card: the page's own heading binds its title.
  yield* ctx.all(
    card.title.transform.scale.tween(1.4, {
      duration: 0.8,
      easing: Easing.outCubic,
    }),
    card.title.transform.rotation.tween(-6, {
      duration: 0.8,
      easing: Easing.outCubic,
    }),
  );
  yield* card.title.appearance.opacity.tween(0.3, 0.6);
  yield* ctx.wait(0.4);

  return { card };
});
// @filename: page-heading.render.tsx
// ---cut---
import { createRenderRuntime } from "@motionactor/renderers/react";
import { actor, dom } from "@motionactor/dom/react";
import type { scene } from "./page-heading.scene";

export const runtime = createRenderRuntime({
  backends: [dom],
  renderers: [],
});

// The page's markup for a build, rendered as SceneRenderer's children.
// The scene's DOM layer writes the heading each frame.
export const page = (compiled: ReturnType<typeof scene.build>) => (
  <div className="grid h-[720px] place-items-center">
    <actor.h1
      of={compiled.scene.card.title}
      className="text-9xl font-medium text-[var(--stage-ink)]"
    >
      Launch
    </actor.h1>
  </div>
);
// @filename: stage.tsx
import { SceneRenderer } from "@motionactor/renderers/react";
import type { scene } from "./page-heading.scene";
import { page, runtime } from "./page-heading.render";

type Build = ReturnType<typeof scene.build>;

export const Stage = ({ compiled, frame }: { compiled: Build; frame: number }) => (
  <SceneRenderer compiled={compiled} frame={frame} width={1280} height={720} runtime={runtime}>
    {page(compiled)}
  </SceneRenderer>
);
```

Vue renders `<actor.h1 :of="compiled.scene.card.title" class="text-6xl">` in a
`<script setup>` template, with `actor` from `@motionactor/dom/vue`, in `SceneRenderer`'s
default slot. Svelte renders `<actor.h1 of={compiled.scene.card.title} class="text-6xl">`, with
`actor` from `@motionactor/dom/svelte`, among its children.

The node is written exactly as a `refs` binding writes one (the table above), and keeps its
place in the page's flow. The server render and the first render carry the frame's style
inline, after the element's own `style`, with the actor's inline effects folded in as the node
paints them: a `domStyle` effect that fades and blurs renders its opacity and `blur(…)` filter
on the server too. An effect whose style reads the node's box has no box before the node
mounts and is measured, so the server render carries its box-less form and the node takes the
measured one on its first frame. Before the actor spawns, the element's own values, hidden.
`of` takes a live handle of a LayoutActor: a frame's resolved actor, or an actor with no
layout, is a type error, and each tag takes its own element's props.

| The scene | Its page markup renders in |
| --- | --- |
| The runtime's default backend is DOM | The default layer, mounted for the markup even when none of its actors paint |
| Another backend paints the default layer | The first declared layer DOM paints |
| No layer is DOM | Beneath every layer: an `actor.<tag>` there throws, naming the actor |

The markup renders at its layer's origin, beneath the layer's actors and under its camera;
layer order and clipping stay as declared. To render part of it in a later DOM layer, wrap that
part in `InLayer` with the build's handle on the layer, `compiled.layers.<key>`; markup with no
address stays in the first DOM layer.

```tsx twoslash title="badge.tsx"
// @jsx: react-jsx
// @filename: badge.scene.ts
import { createScene, layer, LayoutActor } from "@motionactor/core";
import { dom } from "@motionactor/dom";

export class Label extends LayoutActor {
  static type = "label";
}

export const compiled = createScene(
  function* (ctx, _inputs, _viewport, { page, overlay }) {
    const lead = page.spawn(Label);
    const badge = overlay.spawn(Label);
    yield* ctx.wait(1);
    return { lead, badge };
  },
  { layers: { page: layer(dom), overlay: layer(dom) } },
).build({ viewport: { width: 1280, height: 720 }, fps: 30 });
// @filename: badge.tsx
// ---cut---
import { createRenderRuntime, InLayer, SceneRenderer } from "@motionactor/renderers/react";
import { actor, dom } from "@motionactor/dom/react";
import { compiled } from "./badge.scene";

const runtime = createRenderRuntime({ backends: [dom], renderers: [] });

export const Stage = ({ frame }: { frame: number }) => (
  <SceneRenderer compiled={compiled} frame={frame} width={1280} height={720} runtime={runtime}>
    <actor.h2 of={compiled.scene.lead}>Launch</actor.h2>
    <InLayer layer={compiled.layers.overlay}>
      <actor.p of={compiled.scene.badge}>New</actor.p>
    </InLayer>
  </SceneRenderer>
);
```

Vue writes `<InLayer :layer="compiled.layers.overlay">` in the default slot, and Svelte
`<InLayer layer={compiled.layers.overlay}>`, each from its `@motionactor/renderers` entry.
`compiled.layers` holds one handle per declared layer, typed by its backend: a layer another
backend paints, a key string, or a layer of another build does not pass. An `InLayer` may sit
anywhere in the page markup outside an actor's view, inside your own components too. Its markup
renders in the layer it addresses, and the server renders it there too. Changing its `layer`
moves the markup. In React, context provided between `SceneRenderer` and an `InLayer` does not
reach the moved markup, so provide it above `SceneRenderer`. An `InLayer` inside another layer's
moved markup, or inside an actor's view, throws.

`compiled.layers` holds the built scene's own layers. A staged scene's, at any depth, come from
`compiled.layersOf(scene)`, keyed by the definition staged. Two scenes that declare the same key
stay apart, and each handle is typed by its own scene's backend. The markup renders only while
that scene is on stage.

```tsx twoslash title="staged.tsx"
// @jsx: react-jsx
// @filename: staged.scene.ts
import { createScene, layer, LayoutActor } from "@motionactor/core";
import { dom } from "@motionactor/dom";
import { three } from "@motionactor/three/react";

export class Label extends LayoutActor {
  static type = "label";
}

const intro = createScene(function* (ctx) {
  yield* ctx.wait(1);
}, { layers: { overlay: layer(three) } });

export const outro = createScene(function* (ctx, _inputs, _viewport, { overlay }) {
  const badge = overlay.spawn(Label);
  yield* ctx.wait(1);
  return { badge };
}, { layers: { overlay: layer(dom) } });

export const compiled = createScene(function* (ctx) {
  yield* ctx.stage(intro);
  return yield* ctx.stage(outro);
}, { inputs: {} }).build({ viewport: { width: 1280, height: 720 }, fps: 30 });
// @filename: staged.tsx
// ---cut---
import { createRenderRuntime, InLayer, SceneRenderer } from "@motionactor/renderers/react";
import { actor, dom } from "@motionactor/dom/react";
import { compiled, outro } from "./staged.scene";

const runtime = createRenderRuntime({ backends: [dom], renderers: [] });

const Overlay = () => (
  <InLayer layer={compiled.layersOf(outro).overlay}>
    <actor.p of={compiled.scene.badge}>New</actor.p>
  </InLayer>
);

export const Stage = ({ frame }: { frame: number }) => (
  <SceneRenderer compiled={compiled} frame={frame} width={1280} height={720} runtime={runtime}>
    <section>
      <Overlay />
    </section>
  </SceneRenderer>
);
```

An `actor.<tag>` is checked as it binds, before its actor spawns and on the server too. It
throws, naming the actor, outside any `SceneRenderer`; for a handle of another build, even one
whose id this build shares; and for an actor that paints in a layer other than the markup's.
For that last case, wrap the markup in its layer's `InLayer`, spawn the actor through the
markup's layer, or bind it from a view. Whether the actor is present only shows or hides the
node. It owns nothing: the actor's element, if it has one, is still its view's.

## A render method on the class

A DOM actor can carry its own `render` method instead of a separate renderer. The DOM backend
paints it with no registration, and a registered renderer for the class still takes
precedence. Inside it, `this.<signal>()` reads the signal's value at the frame being drawn.

```tsx twoslash title="note.tsx"
// @jsx: react-jsx
// ---cut---
import { LayoutActor, signal } from "@motionactor/core";

export class Note extends LayoutActor {
  text = signal("");

  render(this: { text(): string }) {
    return <p className="note">{this.text()}</p>;
  }
}
```

A class with a `render` method counts as a DOM renderer when a layer decides what it can
paint, so spawn it into a DOM layer.
