---
title: Creating actors
description: One actor from its state to its renderer, with signals for what the scene writes, derived values for what gets drawn and commands for motion.
---

This page builds a `Toast`: a notification that fades in, counts down with a shrinking bar,
fades out and announces that it was dismissed. Each step links to the reference page for the
primitive it uses.

## State

```ts twoslash title="toast.ts"
import {
  derived,
  LayoutActor,
  layoutGroup,
  signal,
  type Duration,
} from "@motionactor/core";

type ToastEvents = {
  dismissed: { message: string };
};

export class Toast extends LayoutActor<ToastEvents> {
  layout = layoutGroup({ width: 360, height: 64 });
  message = signal<string>();
  tone = signal<"info" | "error">("info");
  remaining = signal(1);

  barWidth = derived(() => this.remaining() * this.localLayout().width);

  *show({ hold }: { hold: Duration }) {
    yield* this.appearance.opacity.tween(1, 0.2);
    yield* this.remaining.tween(0, hold);
    yield* this.appearance.opacity.tween(0, 0.2);
    yield* this.emit.dismissed({ message: this.message() });
  }
}
```

Every field the scene writes is a [`signal`](/docs/reactivity/signal), and every signal is also a
spawn input. `message` has no initial value, so a spawn without one is a type error.

`LayoutActor` already declares the `layout`, `transform`, `appearance`, `compositing`,
`flexItem` and `gridItem` groups. `layoutGroup` redeclares `layout` with a default box of
360 by 64, and the other axes keep their defaults. [LayoutActor](/docs/actors/layout-actor)
lists the groups and how authored layout becomes the resolved box.

State says what the toast is doing, in its own terms. `remaining` runs from 1 to 0 and knows
nothing about pixels, and `tone` names a state whose color the renderer picks. A renderer on
any backend can decide what those look like.

## Values to draw

`barWidth` is a [`derived`](/docs/reactivity/derived): it reads `remaining` and the resolved box
at the frame being resolved. The renderer reads the finished number. Geometry computed on the
actor is the same on every backend, and a test can assert it without drawing anything.

## Commands

`show` is a command, a generator method the scene runs with `yield*`. Its durations are
seconds, and it takes an object, so a call reads `toast.show({ hold: 2 })`. Give a command an
object parameter whenever it takes more than one primitive value.

The last step emits `dismissed`. The toast does not know who is listening; a scene that
should react to it [subscribes](/docs/reactivity/events-and-subscribe). To show `show` in an
editor's command list with its parameters, label it with [`command`](/docs/actors/command).

```ts twoslash title="toast.scene.ts"
// @filename: toast.ts
import {
  derived,
  LayoutActor,
  layoutGroup,
  signal,
  type Duration,
} from "@motionactor/core";

type ToastEvents = {
  dismissed: { message: string };
};

export class Toast extends LayoutActor<ToastEvents> {
  layout = layoutGroup({ width: 360, height: 64 });
  message = signal<string>();
  tone = signal<"info" | "error">("info");
  remaining = signal(1);

  barWidth = derived(() => this.remaining() * this.localLayout().width);

  *show({ hold }: { hold: Duration }) {
    yield* this.appearance.opacity.tween(1, 0.2);
    yield* this.remaining.tween(0, hold);
    yield* this.appearance.opacity.tween(0, 0.2);
    yield* this.emit.dismissed({ message: this.message() });
  }
}
// @filename: toast.scene.ts
// ---cut---
import { createScene } from "@motionactor/core";
import { Toast } from "./toast";

export const compiled = createScene(function* (ctx) {
  const toast = ctx.spawn(Toast, {
    message: "Saved",
    appearance: { opacity: 0 },
  });
  yield* toast.show({ hold: 2 });
  return { toast };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });

compiled.totalDuration; // 72
compiled.at(36).scene.toast?.barWidth; // 180
```

The fade in takes 6 frames, the countdown 60 and the fade out 6. At frame 36 the countdown
is halfway, so the bar is half of 360.

## Drawing it

A renderer turns the resolved toast into what one backend draws:

```tsx twoslash title="toast-renderer.tsx"
// @jsx: react-jsx
// @filename: toast.ts
import {
  derived,
  LayoutActor,
  layoutGroup,
  signal,
  type Duration,
} from "@motionactor/core";

type ToastEvents = {
  dismissed: { message: string };
};

export class Toast extends LayoutActor<ToastEvents> {
  layout = layoutGroup({ width: 360, height: 64 });
  message = signal<string>();
  tone = signal<"info" | "error">("info");
  remaining = signal(1);

  barWidth = derived(() => this.remaining() * this.localLayout().width);

  *show({ hold }: { hold: Duration }) {
    yield* this.appearance.opacity.tween(1, 0.2);
    yield* this.remaining.tween(0, hold);
    yield* this.appearance.opacity.tween(0, 0.2);
    yield* this.emit.dismissed({ message: this.message() });
  }
}
// @filename: toast-renderer.tsx
// ---cut---
import { dom } from "@motionactor/dom/react";
import { Toast } from "./toast";

export const toastRenderer = dom.render(Toast, ({ actor }) => (
  <div role="status" className="toast" data-tone={actor.signals.tone}>
    {actor.signals.message}
    <i className="toast-bar" style={{ width: actor.signals.barWidth }} />
  </div>
));
```

`actor.signals` holds each field's value at this frame, typed from the class. The DOM backend
has already sized and placed the element from the resolved box, so the renderer only fills
it. Class names stay static; values that change per frame go through `style`, and states
such as `tone` go through attributes a stylesheet can match.

List the renderer in a render runtime to use it. [Renderers](/docs/rendering/renderers) covers
matching, registries and the alternative for DOM actors, a `render` method on the class.

## Parts

An actor with parts of its own declares them as fields. A [`child`](/docs/runtime/children) is a
fixed part that every toast has, such as an icon. A [`slot`](/docs/runtime/slots) is a place the
scene fills, such as an action button that only some toasts show.

Actor families meant for reuse across projects ship through the
[component library](/docs/component-library). An actor that one composition needs lives next to that
composition.
