Channels

The host-published live-value primitive. Something outside the timeline owns the value and publishes it; resolution reads the publication.

A channel is how an observed value enters a scene without breaking determinism. The timeline never writes it — a renderer, an adapter or an external party publishes, and frame resolution reads through a defined precedence ladder.

Channel vs signal

  • signal — timeline-authored state. The scene generator owns it; writes are compile-time events replayed deterministically by at(frame).
  • channel — host-published state. A renderer, adapter or external party publishes the value at run time.

Rule of thumb

If the value's history is authored (set / tween / play), it is a signal. If the value is observed — measured, fetched, simulated, sensed — it is a channel.

Declaring

class VideoSurface extends LayoutActor {
  // No default: reads throw until the first publication.
  playbackTime = channel<number>();

  // With default: reads return the default until published.
  volume = channel({ default: 1 });

  // Frame-indexed recording (see below).
  measuredBox = channel<ResolvedLayoutBox>({ record: true });
}

Declared as an actor field, a channel is spawn-scoped and keyed `${actorId}:${name}`. Declared free — in a scene body or at module top level — it keys by its enclosing scope and is shared by reference.

Read precedence

Every read resolves in this order. The first layer that applies wins.

Per-call override

at(frame, { channelValues }) pins a value for that resolution only. The publication store is untouched, which is what makes this safe to call during React rendering.

Run log at frame

For record: true channels, the value at the greatest recorded frame ≤ the requested one.

Publication store

A host published a value this run.

Registry

An authored or registered cell value.

Declared default

channel({ default }).

Throw

No default and nothing published.

Writing

// Via the resolved handle
actor.channels.playbackTime.write(3.2);

// By id — the form adapters (physics, renderers, sensors) use
publishToChannel(actorId, "playbackTime", 3.2, { frame });

// Retract, so reads fall back down the precedence ladder
unpublishChannel(actorId, "playbackTime");

write is a publication, not a timeline event. It creates no authored history, which is why set and tween do not exist on channel handles — those are signal verbs and would imply a history the channel does not have.

Recorded channels

By default publications are latest-wins: resolution at any frame sees the most recent value. That is wrong for values that are a function of the frame — measured boxes, stepped physics — because seeking backwards would read a value from the future.

record: true opts into a frame-indexed run log. Each publication records (frame, value) and a read at frame N returns the value at the greatest recorded frame ≤ N, so scrubbing backwards replays what was actually published.

measuredBox = channel<ResolvedLayoutBox>({ record: true });

Demand-gated publication

Producing a channel value can be expensive — DOM measurement forces layout. Producers can gate on demand:

  • demandChannel(actorId, name) declares refcounted intent at consumer mount.
  • Reading through a resolved handle registers sticky observed demand.
  • Producers check hasChannelDemand / subscribeChannelDemand.

Gating is producer-opt-in and degrades safely: a leaked demand refcount means "always publish", never "value missing".

Per-resolution overrides

The safest way for a renderer to supply a host value is per call, not by publishing:

const state = compiled.at(frame, {
  channelValues: new Map([[createCellKey(card.id, "availableWidth"), width]]),
});

Nothing is written to the store, so repeating the frame with the same inputs produces the same output regardless of publication order. The Remotion example uses exactly this.

Measured-box conventions

Layout reads two channel names by convention, exported as constants:

  • MEASURED_BOX — the renderer-measured DOM box for a flow-hosted actor.
  • TARGET_MEASURED_BOX — published by an offscreen target subtree during a layout transition.

FLIP-style projection compares the two frozen measurements and animates between them. See layout transition.

On this page