Layout

Rect is both a shape and a flex layout container. Set flow to arrange its children automatically.

Layout modes

flowBehavior
'horizontal'Children placed side by side (left to right)
'vertical'Children stacked top to bottom
'freeform'Children overlapping, centered by default

Horizontal

import { createScene, Rect } from 'motion-script';

export default createScene(function* (stage) {
  stage.add(
    <Rect flow="horizontal" gap={24} padding={32} fill="#1e293b" cornerRadius={16}>
      <Rect width={80} height={80} fill="tomato"    cornerRadius={8} />
      <Rect width={80} height={80} fill="royalblue" cornerRadius={8} />
      <Rect width={80} height={80} fill="seagreen"  cornerRadius={8} />
    </Rect>
  );
});

Vertical

stage.add(
  <Rect flow="vertical" gap={16} padding={24} fill="#1e293b" cornerRadius={12}>
    <Rect width="fill" height={60} fill="royalblue" cornerRadius={8} />
    <Rect width="fill" height={60} fill="tomato"    cornerRadius={8} />
    <Rect width="fill" height={60} fill="seagreen"  cornerRadius={8} />
  </Rect>
);

Freeform

Children are layered in z-order, centered within the container:

stage.add(
  <Rect flow="freeform" width={300} height={300}>
    <Rect width={300} height={300} fill="royalblue" cornerRadius={16} />
    <Text text="Stacked" fontSize={32} fontWeight={700} fill="white" />
  </Rect>
);

Gap

gap sets the space between children along the layout axis:

<Rect flow="horizontal" gap={32}>...</Rect>
<Rect flow="vertical" gap={8}>...</Rect>

Pass gap="auto" to distribute children evenly (space-between):

<Rect flow="horizontal" gap="auto" width={600} height={80} padding={24}>
  <Rect width={60} height={60} fill="tomato" />
  <Rect width={60} height={60} fill="royalblue" />
  <Rect width={60} height={60} fill="seagreen" />
</Rect>

Alignment

align controls child placement on the cross axis. Pass a named position ('center', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight', 'topCenter', 'bottomCenter', 'centerLeft', 'centerRight') or an explicit { x, y } pivot. On each axis, x is -1 (left) … 1 (right) and y is -1 (bottom) … 1 (top); 0 centers. It defaults to 'center'.

// Horizontal: cross axis is vertical — align children to the top
<Rect flow="horizontal" gap={20} align="topCenter">
  <Rect width={60} height={120} fill="tomato" />
  <Rect width={60} height={60}  fill="royalblue" />
</Rect>

// Vertical: cross axis is horizontal — x: 1 aligns children to the right
<Rect flow="vertical" gap={12} align={{ x: 1, y: 0 }}>
  <Rect width={200} height={50} fill="tomato" />
  <Rect width={100} height={50} fill="royalblue" />
</Rect>

Padding

Inset the layout's content area from the container's edges:

// Uniform
<Rect flow="vertical" gap={12} padding={24}>...</Rect>

// Per-side
<Rect flow="vertical" gap={12} padding={{ top: 32, bottom: 32, left: 16, right: 16 }}>...</Rect>

// Symmetric shorthand
<Rect flow="horizontal" gap={16} padding={{ horizontal: 24, vertical: 16 }}>...</Rect>

Positioning: relative vs. absolute

By default a node is placed by its parent — measured against the parent's content box, given a cell by the parent's flow, and offsetting from that cell with its own x/y. Two props let a subtree opt out of that and be placed against the stage instead.

PropValuesDefaultSet on
childPositioning'relative' | 'absolute''relative'the container — the frame it hands every child
relativeToParent'inherit' | 'relative' | 'absolute''inherit'one child — its override of the above

An absolute node steps out of its parent's layout entirely: it consumes no gap, takes no flex share, and never contributes to a hug size. Its x/y become plain scene coordinates, and 'fill' fills the whole stage:

<Rect flow="vertical" gap={16} padding={32} width="hug">
  <Rect width={200} height={80} fill="royalblue" />
  <Rect width={200} height={80} fill="seagreen" />

  {/* Pinned to the scene, not to the column: (0,0) is the stage centre. */}
  <Text text="WATERMARK" relativeToParent="absolute" y={-420} opacity={0.3} />
</Rect>

Flip the container instead when everything inside it should be placed by hand — a free canvas rather than an auto-layout frame:

<Rect childPositioning="absolute" width="fill" height="fill">
  <Rect x={-300} y={120} width={120} height={120} fill="tomato" />
  <Rect x={280}  y={-60} width={120} height={120} fill="royalblue" />
</Rect>

The node still belongs to its parent — it renders inside that scope, so the parent's opacity, blend, clip, effects, rotation and scale all still apply. What changes is only where its box is anchored.

Both props animate with .to(), snapping at the end of the tween like any other discrete mode. One thing to watch when switching back: x/y are scene coordinates while a node is absolute and an offset from its layout cell once it isn't, so a node carrying a large offset lands far from its new slot. Ease them to zero as it rejoins:

badge().set({ relativeToParent: 'relative' });
yield* badge().to({ x: 0, y: 0 }, 0.9, easeInOut('quad'));

Nesting layouts

Layouts can be nested arbitrarily deep:

stage.add(
  <Rect flow="vertical" gap={16} padding={32} fill="#0f1117" width="fill" height="fill">
    <Rect flow="horizontal" gap={16} width="fill">
      <Rect width="fill" height={200} fill="#1e293b" cornerRadius={12} />
      <Rect width="fill" height={200} fill="#1e293b" cornerRadius={12} />
    </Rect>
    <Rect width="fill" height={120} fill="#1e293b" cornerRadius={12} />
  </Rect>
);

Animating layout props

All layout props are animatable with .to():

import { createScene, Rect, createRef, easeOut } from 'motion-script';

export default createScene(function* (stage) {
  const container = createRef<Rect>();

  stage.add(
    <Rect ref={container} flow="horizontal" gap={8} padding={24} fill="#1e293b" cornerRadius={16}>
      <Rect width={80} height={80} fill="tomato"    cornerRadius={8} />
      <Rect width={80} height={80} fill="royalblue" cornerRadius={8} />
      <Rect width={80} height={80} fill="seagreen"  cornerRadius={8} />
    </Rect>
  );

  // Expand gap
  yield* container().to({ gap: 48 }, 0.6, easeOut);

  // Switch to a vertical layout
  yield* container().to({ flow: 'vertical' }, 0.5, easeOut);
});

Animated child insertion and removal

Children can be added or removed with animated transitions:

const card = new Rect({ width: 200, height: 80, fill: '#4f80ff', cornerRadius: 8 });

// Insert at index 1 with a 0.4-second fade/grow
yield* container().addChildAt(card, 1, 0.4, easeOut);

// Remove at index 0 with a 0.3-second fade/shrink
yield* container().removeChildAt(0, 0.3);