---
title: channel
description: A value the host publishes while frames play, such as a measured width, read when a frame resolves.
---

A channel holds a value the scene cannot author: the width a browser gives a label, a value a
host measures while the scene plays. An actor declares it and computes from it in a
`derived`:

```ts twoslash title="meter.ts"
import { channel, derived, LayoutActor, signal } from "@motionactor/core";

export class Meter extends LayoutActor {
  level = signal(0.5);
  trackWidth = channel({ default: 400 });

  fillWidth = derived(() => this.level() * this.trackWidth());
}
```

The host writes it through the resolved actor's handle, and the same compiled frame resolves
to the new value:

```ts twoslash
// @filename: meter.ts
import { channel, derived, LayoutActor, signal } from "@motionactor/core";
export class Meter extends LayoutActor {
  level = signal(0.5);
  trackWidth = channel({ default: 400 });
  fillWidth = derived(() => this.level() * this.trackWidth());
}
// @filename: meter.scene.ts
// ---cut---
import { createScene } from "@motionactor/core";
import { Meter } from "./meter";

const compiled = createScene(function* (ctx) {
  const meter = ctx.spawn(Meter, {}, { id: "meter" });
  yield* ctx.wait(1);
  return { meter };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });

compiled.at(0).scene.meter?.fillWidth; // 200
const trackWidth = compiled.at(0).scene.meter?.channels.trackWidth;

trackWidth?.write(800);
compiled.at(0).scene.meter?.fillWidth; // 400

trackWidth?.unpublish();
compiled.at(0).scene.meter?.fillWidth; // 200
```

The handle is the only way to write a channel. The scene script never writes one: `write`
inside a generator throws, because a script authors the timeline and a channel has none.

## Publishing from a renderer

A renderer reads channel values from `actor.signals`, like any other field. The component it
returns publishes with `useChannelPublication(handle, produce)`:

```tsx twoslash title="label.tsx"
// @jsx: react-jsx
// ---cut---
import {
  channel,
  LayoutActor,
  signal,
  type ResolvedChannelHandle,
} from "@motionactor/core";
import { dom } from "@motionactor/dom/react";
import { useChannelPublication } from "@motionactor/renderers/react";
import { useRef } from "react";

export class Label extends LayoutActor {
  text = signal("");
  textWidth = channel({ default: 0, record: true });
}

function LabelText(props: { text: string; width: ResolvedChannelHandle<number> }) {
  const ref = useRef<HTMLSpanElement>(null);
  useChannelPublication(props.width, () => ref.current?.offsetWidth ?? null);
  return <span ref={ref}>{props.text}</span>;
}

export const label = dom.render(Label, ({ actor }) => (
  <LabelText text={actor.signals.text} width={actor.channels.textWidth} />
));
```

After each commit it calls `produce` and writes the result with the frame the scene renderer
committed; `null` skips the commit. It removes the publication when the component unmounts.
The renderer function itself calls no hooks, so the publication lives in the component.
The Vue and Svelte bindings export their own `useChannelPublication`.

## Declaring

| Declaration | Before anything is published |
| --- | --- |
| `channel({ default })` | Reads return the default |
| `channel<T>()` | The resolved actor leaves the field out, and a read throws |

Had `trackWidth` no default, reading it before a publication would throw
`Channel "meter:trackWidth" cannot be read before publication for frame 0.` Declare a
default when a frame can resolve before the producer has run, as on the first paint.

Two options change how publications are kept. `record: true` keeps a value per frame,
described [below](#recorded-channels). `equals(previous, next)` decides when a write is no
change: an equal write leaves the value and the publication version alone, so cached frames
stay valid. The measured box on every `LayoutActor` uses it to ignore movement under half a
pixel.

## Where a channel lives

A channel lives where it is declared:

| Declared | Lives in | Shared by |
| --- | --- | --- |
| As an actor field | The actor, in the build that spawned it | Everything holding the actor |
| In a scene or composition script | That build | Everything in the build holding the reference |
| At module scope | The module | Every build that reads it |

Every compile is its own build. Two builds of one definition each get their own channel, even
for actors with the same id, and writing one leaves the other alone:

```ts twoslash
// @filename: meter.ts
import { channel, derived, LayoutActor, signal } from "@motionactor/core";
export class Meter extends LayoutActor {
  level = signal(0.5);
  trackWidth = channel({ default: 400 });
  fillWidth = derived(() => this.level() * this.trackWidth());
}
// @filename: builds.ts
// ---cut---
import { createScene } from "@motionactor/core";
import { Meter } from "./meter";

const gauge = createScene(
  function* (ctx) {
    const meter = ctx.spawn(Meter, {}, { id: "meter" });
    yield* ctx.wait(1);
    return { meter };
  },
  { inputs: {} },
);

const viewport = { width: 1280, height: 720 };
const left = gauge.build({ inputs: {}, viewport, fps: 30 });
const right = gauge.build({ inputs: {}, viewport, fps: 30 });

left.at(0).scene.meter?.channels.trackWidth.write(800);
left.at(0).scene.meter?.fillWidth; // 400
right.at(0).scene.meter?.fillWidth; // 200
```

A module channel is the one value every build reads. A compiled scene reads it live, so a
write after the compile reaches every frame resolved after it:

```ts twoslash
import { channel, createScene, derived, LayoutActor } from "@motionactor/core";

const accent = channel({ default: 1 });

class Bar extends LayoutActor {
  glow = derived(() => accent() * 2);
}

const compiled = createScene(function* (ctx) {
  const bar = ctx.spawn(Bar);
  yield* ctx.wait(1);
  return { bar };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });

compiled.at(0).scene.bar?.glow; // 2
accent.write(5);
compiled.at(0).scene.bar?.glow; // 10
```

A build's channels go with it. Once it is disposed, with `using` or `[Symbol.dispose]()`, its
handles throw on write and `at` throws; see
[Compiled scenes](/docs/runtime/compiled-scene#releasing-a-build).

## Which value a read gets

A read takes the first of these that has a value:

1. for a recorded channel, the value recorded at the latest frame at or before this one,
2. the latest publication,
3. the declared default.

With none of them, the read throws. An actor's `measuredBox` is the one exception, described
[below](#measured-boxes).

## Recorded channels

A channel without `record` holds one value, the latest. That suits a pointer or a sensor,
which are live. It is wrong for a value that depends on the frame, such as a measured box
that changes as an actor animates: after scrubbing back, every frame would read the box from
the last frame painted.

`record: true` keeps a log. A write with a frame, `handle.write(value, frame)`, is stored at
that frame, and a read at frame N returns the value at the latest recorded frame at or before
N. `useChannelPublication` passes the committed frame for you. Publishing at the same frame
again replaces the entry. A write without a frame only updates the latest value, which reads
fall back to before the first recorded frame.

## Demand

Producing a value can be expensive. Measuring the DOM forces a layout. A producer can publish
only while something reads the channel, and reading is what asks for it: a `derived` that
calls one of the build's channels while a frame resolves, or a call to
`actor.channels.<name>()`. The request stays until the run is reset. The runtime's own reads,
such as layout checking for a measured box, do not ask, and neither does reading a module
channel: nothing would clear its request.

| Call | Side |
| --- | --- |
| `actor.channels.<name>.hasDemand()` | Producer: whether a read asked for the value this run |
| `actor.channels.<name>.subscribeDemand(fn)` | Producer: called when demand starts and when it stops |

## Measured boxes

Every `LayoutActor` declares a recorded channel, `measuredBox`, exported as `MEASURED_BOX`:
the box a DOM renderer measured for the actor. Layout uses it in place of the authored box
for an actor placed by CSS flow.

A `derived` can read it before anything is measured. The read asks the DOM backend to
measure, and until the box is published it returns the box layout computes: the actor's `x`,
`y`, `width` and `height` against its parent's box, or its layer's.

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

class Waveform extends LayoutActor {
  level = signal(0.5);
  bar = derived(() => this.level() * this.measuredBox().height);
}

const compiled = createScene(function* (ctx) {
  const wave = ctx.spawn(Waveform, { layout: { width: 200, height: 80 } });
  yield* ctx.wait(1);
  return { wave };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });

const wave = compiled.at(0).scene.wave;
wave?.bar; // 40, from the layout box
wave?.channels.measuredBox.hasDemand(); // true: the DOM backend measures it
```

The script runs before anything is measured, so reading `measuredBox` there throws. Read it
in a `derived`.

## Channels and watches

A [`watch`](/docs/reactivity/watch) runs while the scene compiles, before anything is published, so
a watch whose source reads a channel throws. Compute from a channel in a
[`derived`](/docs/reactivity/derived), which runs at every resolve.
