---
title: LayoutActor
description: The base class for anything drawn on the stage. It adds a box, a transform, opacity, compositing and a measured size.
---

`LayoutActor` extends [`Actor`](/docs/actors/actor) with the groups every drawn thing needs, so a
class that extends it has a box and can fade, scale, rotate and blend without declaring any
of that.

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

class Badge extends LayoutActor {
  label = signal("Ready");
  layout = layoutGroup({ width: 160, height: 48 });
}
```

Redeclare `layout` with [`layoutGroup`](/docs/reactivity/group#layoutgroup) to change the
default size. Axes you leave out keep their defaults.

## Groups

| Group | Fields | Defaults |
| --- | --- | --- |
| `layout` | `x`, `y`, `width`, `height` | `0` |
| `transform` | `scale`, `rotation` (degrees) | `1`, `0` |
| `appearance` | `opacity` | `1` |
| `shape` | `radius` (px) | `0` |
| `compositing` | `blend`, `isolate`, `clip` | `"source-over"`, `false`, `null` |
| `flexItem` | `alignSelf` | `"auto"` |
| `gridItem` | `gridColumn`, `gridRow` | `null` |

Each field is a signal: read it, `set` it or `tween` it.

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

class Badge extends LayoutActor {}

createScene(function* (ctx) {
  const badge = ctx.spawn(Badge, { layout: { width: 160, height: 48 } });

  yield* ctx.all(
    badge.layout.x.tween(400, 0.6),
    badge.transform.rotation.tween(-8, {
      duration: 0.6,
      easing: Easing.outCubic,
    }),
  );
  badge.appearance.opacity.set(0.5);
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });
```

`shape.radius` rounds the corners of the actor's box and clips what it draws to them; effects
that need the box's outline get it too.

`flexItem` and `gridItem` are read by the actor's host when the host lays its children out
as a flex row or a grid. They do nothing on a root actor.

## Compositing

By default an actor's children paint straight into its parent, one after another. Setting
any `compositing` field away from its default makes the actor a group: it and its subtree are
drawn into one layer, which is then composited onto the parent.

- `blend` is a CSS blend mode such as `"multiply"` or `"screen"`, applied to the group.
- `isolate: true` makes a group with nothing else changed, so the subtree's own blend modes
  stay inside it.
- `clip` clips the group to the actor's box, with that corner radius in pixels.

A group also changes what `appearance.opacity` means. On a group it fades the finished layer,
so overlapping children no longer show through one another. Resolved actors carry these
values as `compositing`, which is what backends read.

## Authored and resolved layout

`layout` is what you write. It takes numbers, which are pixels, and percentage strings such
as `"50%"`. `localLayout` is what the scene resolves it to: always numbers, with percentages
of a root actor resolved against the viewport and those of a hosted actor against its host's
box. `worldLayout` adds the hosts' positions, so it is the box on the stage.

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

class Badge extends LayoutActor {}

const definition = createScene(
  function* (ctx) {
    const badge = ctx.spawn(Badge, {
      layout: { x: "50%", y: "25%", width: "10%", height: 40 },
    });
    badge.localLayout(); // { x: 960, y: 270, width: 192, height: 40 }
    yield* ctx.wait(1);
    return { badge };
  },
  { inputs: {} },
);

const compiled = definition.build({
  viewport: { width: 1920, height: 1080 },
  inputs: {},
  fps: 30,
});
compiled.at(0).scene.badge?.layout.x; // "50%"
compiled.at(0).scene.badge?.localLayout.x; // 960
```

A built scene resolves against the viewport it was built for. A new stage size is a new build.

`bounds()` is the same as `localLayout()`.

## Measured size

Some boxes are known only after the browser lays them out: a paragraph that wraps, an item a
flex row places. Read the box in a `derived`:

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

class Caption extends LayoutActor {
  text = signal("");
  underline = derived(() => this.measuredBox().width);
}
```

The read asks the DOM backend to measure the element of an actor its host lays out by CSS and
publish the box to the actor's `measuredBox` [channel](/docs/reactivity/channel#measured-boxes).
Until the box is published, the read returns the box layout computes from the actor's own `x`,
`y`, `width` and `height`. For every frame that has a measured box, the actor's resolved
`localLayout` is that box instead of the authored one. The channel is recorded, so a seek back
returns the box measured at that frame. See
[Determinism](/docs/runtime/determinism#values-from-the-host).

## Transitions

`*transition(apply, options)` applies a change at the cursor and, for `duration` seconds,
has the renderer morph the view from the frame before the change into the view after it:

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

class Calendar extends LayoutActor {
  view = signal<"month" | "week">("month");

  *showWeek() {
    yield* this.transition(() => this.view.set("week"), {
      duration: 0.5,
    });
  }
}
```

The change itself lands on one frame: `view` is `"week"` from the cursor on. The morph is
drawn from `transitionState`, which every `LayoutActor` carries. See
[Transitions](/docs/rendering/transitions).
