---
title: Events and subscribe
description: An actor announces what happened with a typed event; subscribe attaches another actor's response.
---

An actor emits an event to say what happened, without knowing who is listening. `subscribe`
attaches a response from another actor. The emitter keeps no list of listeners, so the same
actor can drive different responses in different scenes.

```ts twoslash title="button.ts"
import { LayoutActor } from "@motionactor/core";

type ButtonEvents = {
  pressed: { via: "key" | "pointer" };
};

export class Button extends LayoutActor<ButtonEvents> {
  *press(via: "key" | "pointer") {
    yield* this.appearance.opacity.tween(0.6, 0.1);
    yield* this.emit.pressed({ via });
    yield* this.appearance.opacity.tween(1, 0.1);
  }
}
```

The type parameter of `Actor` or `LayoutActor` is the event map: each key is an event name and
its value is the payload. `this.emit` is typed from it, so `this.emit.pressed` takes exactly
`{ via }` and an unknown event name does not compile.

```ts twoslash
// @filename: button.ts
import { LayoutActor } from "@motionactor/core";
type ButtonEvents = {
  pressed: { via: "key" | "pointer" };
};
export class Button extends LayoutActor<ButtonEvents> {
  *press(via: "key" | "pointer") {
    yield* this.appearance.opacity.tween(0.6, 0.1);
    yield* this.emit.pressed({ via });
    yield* this.appearance.opacity.tween(1, 0.1);
  }
}
// @filename: form.scene.ts
// ---cut---
import {
  createScene,
  LayoutActor,
  signal,
  subscribe,
} from "@motionactor/core";
import { Button } from "./button";

class Form extends LayoutActor {
  sentVia = signal("");

  *submit(via: string) {
    yield* this.appearance.opacity.tween(0.5, 1);
    this.sentVia.set(via);
  }
}

const compiled = createScene(function* (ctx) {
  const button = ctx.spawn(Button);
  const form = ctx.spawn(Form);

  subscribe(form, button, {
    *pressed({ via }) {
      yield* this.submit(via);
    },
  });

  yield* button.press("key");
  return { form };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });

compiled.totalDuration; // 33
compiled.at(33).scene.form?.sentVia; // "key"
```

## Emitting

`yield* this.emit.name(payload)` takes no time. It records the event at the cursor and starts
every handler subscribed to it, then the emitter carries on. Each emit is also kept in
`compiled.events`, as an `actor-event` entry with the actor, the name, the payload and the
frame.

## Subscribing

`subscribe(subscriber, source, handlers)` takes the actor that responds, the actor whose events
it hears, and a handler per event. Handlers are optional per event, and inside one `this` is
the subscriber.

A plain function handler runs at the emit, before the emitter continues. A generator handler
runs on a branch of its own from the emit frame. The emitter does not wait for it: above, the
button's press is over at frame 6 while the form's `submit` runs to frame 33, and the scene
lasts until the last branch ends. At a frame the handler shares with the emitter, the handler
steps first ([reads across branches](/docs/runtime/scene-orchestration#reads-across-branches)). A
handler stops when its subscriber exits.

Call `subscribe` while the scene compiles: in the scene script, in a command, or in a host's
`onInit` to wire up its [children](/docs/runtime/children). Anywhere else it throws
`Actor subscriptions can only be created while attached to a scene.` A subscription hears
only the events emitted after it was made.

## Who changes what

The source states what happened and the subscriber decides what it means for its own state.
A dock emits `itemActivated`, and the desktop that owns the windows opens one:

```ts twoslash
import { LayoutActor, signal, subscribe } from "@motionactor/core";

type DockEvents = { itemActivated: { appId: string } };
class Dock extends LayoutActor<DockEvents> {}

class Desktop extends LayoutActor {
  windows = signal<string[]>([]);

  *openWindow(appId: string) {
    this.windows.set([...this.windows(), appId]);
    yield* this.appearance.opacity.tween(1, 0.2);
  }
}

declare const dock: Dock;
declare const desktop: Desktop;
// ---cut---
subscribe(desktop, dock, {
  *itemActivated({ appId }) {
    yield* this.openWindow(appId);
  },
});
```

Had the dock set `desktop.windows` itself, it would also have to know every rule that comes
with opening a window, such as focusing it and moving it to the front. With the event, those
rules stay in `Desktop`, and a dock in a scene without a desktop still works.

Sound uses the same mechanism. Actors emit events and know nothing about audio, and the scene
decides which events play which sounds with `bindSounds`, a subscription made by the
[audio manager](/docs/actors/audio).
