Skip to content
MotionActor
Esc
↑↓navigate↵open⌘Jpreview
On this page

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:

motionactor
packages/primitives — generic runtime built-ins
motionactor-library
reusable actor families, registry items, showcase demos
motionactor-lab
private experiments

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:

For Use
Flat state signal
Nested state bags reactiveSignal
Grouped state group / layoutGroup
Computed reads derived
Host-published values channel
Coordination events and subscribe
Local reactive response watch
Owned semantic children child
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.

Was this page helpful?