Creating actors

Where an actor belongs, how to lay out its state, what its render path must be, and the rules that keep it reusable.

1. Choose the owner

Three repositories, and the choice is not negotiable by convenience:

packages/primitives — generic runtime built-ins

Keep the actor, view, renderer and feature exports colocated. Do not create a family folder in the runtime repo to work around a missing library abstraction — that is the abstraction asking to be written.

2. Implement the actor

Use the primitives intentionally. The Reactivity section has the full decision table; the short version:

ForUse
Flat statesignal
Nested state bagsreactiveSignal
Grouped stategroup / layoutGroup
Computed readsderived
Host-published valueschannel
Coordinationevents and subscribe
Local reactive responsewatch
Owned semantic childrenchild
import { LayoutActor, group, layoutGroup, signal } from "@motionactor/core";

export class Image extends LayoutActor {
  layout = layoutGroup({
    width: 320,
    height: 180,
  });

  content = group({
    src: signal(""),
    objectFit: signal<"cover" | "contain">("cover"),
  });
}

Rules

  • Authored durations are seconds, never frames.
  • Keep actor state semantic and renderer-agnostic. isHighlighted, not borderColor.
  • Prefer object parameters for commands with more than one scalar argument.
  • Prefer semantic commands over direct signal tweening as the main authoring surface — card.reveal({ duration }) reads better than three tweens at every call site, and it keeps the motion in one place when it changes.
  • LayoutActor already declares layout, transform, appearance, flexItem and gridItem. Override layout = layoutGroup({...}) to change defaults rather than redeclaring the group.

Layout authoring

  • layout is authored input; localLayout and worldLayout are resolved output.
  • layout.x, layout.y, layout.width and layout.height accept numbers, pct(n), or percentage strings like "50%".
  • Root actors resolve percentages against the scene viewport. Nested positioned actors resolve against their containing block — usually the nearest positioned ancestor's local box.
  • Resolved boxes are always numeric, even when the authored value was a percentage.
  • Renderers size their element, SVG, canvas or backend surface from resolved localLayout.

3. Give it a render path

Every visual actor needs one:

  • an actor render(...) method — preferred for React-first visual actors, or
  • a defineActorRenderer(...) renderer — valid for migration, specialization and renderer overrides, or
  • both, when a renderer overrides a default.

When both exist they share one render-time model: actor is resolved frame state, slots is rendered child placement, targets is the render-facing target projection.

Nonvisual actors need behavioral examples instead of a visual demo.

View rules

  • Keep className static.
  • Compute dynamic visual values into local variables and pass them through style.
  • Do not use generic signal-reader helpers or string-key probing when the actor type is known.
  • Do the geometry in derived on the actor, not in the view. The view should read finished numbers — see the Desktop example.

4. Wire the exports

  • Feature barrel: <family>/src/<feature>/index.ts
  • Family package root: <family>/src/index.ts
  • Primitive package root: packages/primitives/src/index.ts

5. Demonstrate it

Visual actors get a showcase composition in the owning library, built with defineSceneComposition(...). Nonvisual actors get a behavioral test instead — see Testing.

Traits were removed

Shared state uses group / layoutGroup; reusable motion is expressed through actor commands and the existing signal and effect primitives. If you find yourself wanting a mixin, you probably want a child actor or a shared command helper.

On this page