---
title: Backends and layers
description: The DOM, Pixi, Three.js and Skia backends, and how a scene splits its stage between them.
---

A backend is a drawing surface: the component that mounts it, the renderers built into it and
the effect kinds it can paint. A layer is a rectangle of the stage that one backend paints. A
scene declares its layers, and a render runtime supplies a backend for each one.

## The backends

| Backend | Import | Mounts | Places each actor | Effect kinds |
| --- | --- | --- | --- | --- |
| DOM | `dom` from `@motionactor/dom/react` | A positioned element per actor | Yes | `domStyle`, `glShader`, `canvasPaint` |
| Pixi | `pixi` from `@motionactor/pixi/react` | A Pixi `Application` with a transparent background | Yes | `pixiFilter` |
| Three.js | `three` from `@motionactor/three/react` | A React Three Fiber canvas | No | `threePass`, `threeMaterial` |
| Skia | `createSkiaBackend` from `@motionactor/skia/react` | A Skia canvas you supply | No | None yet |

"Places each actor" means the backend applies position, size, scale, rotation, opacity and
clipping from the actor's state, so its renderers draw in the actor's local box. On Three.js
and Skia a renderer places what it draws itself. For another drawing surface, see
[Writing a backend](/docs/rendering/writing-a-backend).

None of the backends runs a clock. Pixi is created with `autoStart: false` and React Three
Fiber with `frameloop="never"`. Each draws when the frame changes, then tells the host the
frame is on screen. Three.js sets its clock to `frame / fps` before it advances, so seeking
backwards or repeating a frame gives the same picture.

Each backend also has a ref, a small React-free value under the same name, exported from the
package root beside the backend's actor classes and effect kinds. The React backend, renderers
and views live at `/react`. Scene files import the root to declare layers and effects, so a
scene never pulls a renderer or a canvas into its module graph. A backend is accepted wherever
its ref is, so renderer files hold just the backend:

```ts twoslash
import { dom } from "@motionactor/dom";
import { pixi } from "@motionactor/pixi";
import { three } from "@motionactor/three";
import { skia } from "@motionactor/skia";

[dom.name, pixi.name, three.name, skia.name];
// ["dom", "pixi", "three", "skia"]
```

## The render runtime

`createRenderRuntime` takes the backends an app uses and the renderers it adds. The first
backend is the default unless `default` names another.

```tsx twoslash title="runtime.ts"
import { createRenderRuntime } from "@motionactor/renderers/react";
import { dom } from "@motionactor/dom/react";
import { pixi } from "@motionactor/pixi/react";
import { three } from "@motionactor/three/react";

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

runtime.defaultBackend.name; // "dom"
runtime.hasBackend(three); // true
```

It throws when it is given no backends, two backends with one name, a default it does not
have, or a renderer for a backend it does not have.

## Layers

A scene without layers paints everything into the default layer, which covers the whole
stage and uses the runtime's default backend. Declare layers to split the stage. Each one is
`layer(backend, { frame })`, with the frame in stage pixels; leave the frame out for the full
stage.

```ts twoslash title="title-card.scene.ts"
import { createScene, layer, LayoutActor, signal } from "@motionactor/core";
import { dom } from "@motionactor/dom";
import { three } from "@motionactor/three";

class Globe extends LayoutActor {
  spin = signal(0);
}

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

export const titleCard = createScene(
  function* (ctx, _inputs, viewport, { stage, overlay }) {
    const globe = stage.spawn(Globe);
    overlay.spawn(Caption, {
      text: "Launch day",
      layout: { x: 0, y: viewport.height - 160 },
    });
    yield* globe.spin.tween(1, 3);
    return { globe };
  },
  {
    layers: {
      stage: layer(three, {
        frame: { x: 320, y: 0, width: 1280, height: 1080 },
      }),
      overlay: layer(dom),
    },
  },
);

const compiled = titleCard.build({ viewport: { width: 1920, height: 1080 }, fps: 30 });
compiled.at(0).layers.map((layer) => layer.kind);
// ["default", "declared", "declared"]
```

The script's fourth parameter holds one handle per declared layer, typed from the
declaration. `handle.spawn` takes the same arguments as `ctx.spawn` and places a root actor
in that layer; children paint in their root's layer. Coordinates are local to the layer, so
`x: 0` in `stage` is 320 pixels from the stage's left edge.

A layer's frame resolves against the viewport the scene is built with, so a new stage size is
a new build.

## Paint order

1. Layers: the default layer first, then the declared layers in the order they are written.
   A layer key that is an integer would break that order, so it throws. While a scene is
   staged, its layers paint below its stager's: the deepest staged scene's first, the built
   scene's last. A background therefore goes in the default layer.
   Each layer's `id` is its key, or `key#2`, `key#3` when an earlier scene in the build
   declared the key; an actor's `layer` and the layer's mount key are that id.
2. Root actors within a layer, in spawn order.
3. Children within an actor, in child order. `place.reorder([...])` changes it from a given
   frame on.

There is no z-index. To bring an actor forward, reorder its slot. Adding a layer is the wrong
fix, since a layer is a separate canvas or DOM root.

## What a layer will paint

`SceneRenderer` checks every root before mounting any layer:

| The root, or anything below it, has | Result |
| --- | --- |
| A renderer on the layer's backend, or a class `render` method on a DOM layer | Painted |
| No renderer on any backend | Skipped as logical, such as a clock or a camera |
| Renderers only on other backends | Throws, naming the actor, the layer and the backend |

A declared layer whose backend the runtime lacks also throws. An editor can ask the same
question without rendering through `classifyLayerPlacement` from
`@motionactor/renderers/react`.

Each layer renders as an absolutely positioned box at its frame with `overflow: hidden` and a
`data-layer` attribute holding its id. The scene camera applies inside each layer, and the
backend's host mounts inside that box at the layer's size.
