Size

width and height accept three value kinds: a fixed pixel number, 'fill', or 'hug'.

Fixed size

<Rect width={300} height={200} fill="royalblue" />

'fill': expand to parent

A node with width: 'fill' stretches to consume all available space along that axis in its parent container. Multiple fill siblings share the space equally (or by their flex ratio):

<Rect group="row" gap={16} padding={24} fill="#0f1117" width={600} height={100}>
  <Rect width="fill" height="fill" fill="royalblue" />  {/* takes half */}
  <Rect width="fill" height="fill" fill="tomato" />     {/* takes half */}
</Rect>

Info

Every layout container resolves an unset width/height for you — see Default sizing below for the full rule.


'hug': shrink to content

A node with width: 'hug' measures its children and sizes itself to fit them exactly (plus any padding):

<Rect group="row" gap={12} width="hug" height="hug" padding={20} fill="#1e293b">
  <Rect width={60} height={60} fill="tomato" cornerRadius={8} />
  <Rect width={60} height={60} fill="royalblue" cornerRadius={8} />
</Rect>

Default sizing

Every layout container resolves an unset width/height to one of 'fill' or 'hug' — you never have to set either explicitly for the common case:

ContainerNo childrenHas children
Rectwidth: 'fill', height: 'fill'width: 'hug', height: 'hug'
Row / Columnwidth: 'hug', height: 'hug'width: 'hug', height: 'hug'

Row and Column always default to 'hug' on both axes — they're layout containers first, so an empty one shouldn't stretch and cover the scene the way an empty Rect (a shape that doubles as a background/spacer) does.

A 'fill' child needs somewhere to fill into

'hug' and 'fill' pull in opposite directions on the same axis: 'hug' means "size me from my children," 'fill' means "size me from my parent." If a container hugs an axis while a direct child fills that same axis, the child has nothing to fill into.

To avoid that, the default above gets one refinement: if a direct child is 'fill' on the axis the container would otherwise hug — its main axis, the axis children are laid out along — the container's default for that axis becomes 'fill' too, so the child has real space to expand into.

ContainerMain axisCross axis
Rect group="row" / Rowwidthheight
Rect group="column" / Columnheightwidth
Rect group="stack"width and height, checked independently

A fill child on the cross axis doesn't trigger this — it already anchors correctly to the size of the tallest/widest fixed sibling, which is what lets this column's tiles span its full hugged width without the column itself needing to fill:

<Rect group="column" gap={16} padding={20} fill="#1e293b">
  <Rect width="fill" height={60} fill="tomato" cornerRadius={8} />
  <Rect width="fill" height={60} fill="royalblue" cornerRadius={8} />
</Rect>

But a fill child on the main axis is the case that needs the container's own default to change. Here, the middle tile has no width — with children of its own it would hug, but it has none, so it defaults to 'fill'. Because that's the row's main axis, the row's own default flips from 'hug' to 'fill' too, and the row spans all 1920px of available width instead of collapsing:

<Rect group="row" padding={48} gap={48}>
  <Rect width={400} height={400} fill="#6990DD" />
  <Rect height={400} fill="#E8617C" />   {/* no width → defaults to 'fill' */}
  <Rect width={400} height={400} fill="#F5C26B" />
</Rect>

In a 1920-wide scene, the two fixed tiles and the padding/gaps leave 928px for the middle tile — it fills exactly that, and the row itself resolves to 'fill' (1920px), not 'hug'.

This only changes the inferred default. An explicit width/height on the container always wins, even if it re-creates the hug + fill-with-nothing-to-fill-into combination:

<Rect group="row" width="hug" padding={48} gap={48}>
  <Rect width={400} height={400} fill="#6990DD" />
  <Rect height={400} fill="#E8617C" />   {/* still defaults to 'fill' */}
  <Rect width={400} height={400} fill="#F5C26B" />
</Rect>

Forcing width: 'hug' here means the row genuinely has nothing to hug the middle tile to, so it collapses to 0 width. The row itself still hugs to a finite size from its fixed tiles, padding, and gaps — 992px — rather than growing without bound.


flex: proportional fill

When multiple siblings use width: 'fill' (or height: 'fill'), the flex prop controls how much of the free space each one gets. The default is 1, giving equal shares.

<Rect group="row" gap={8} fill="#0f1117" width={600} height={80}>
  <Rect flex={2} height="fill" fill="royalblue" />  {/* takes 2/3 */}
  <Rect flex={1} height="fill" fill="tomato" />     {/* takes 1/3 */}
</Rect>

Setting flex without an explicit width/height defaults both to 'fill' automatically.


size: shorthand for both axes

size is sugar for setting width and height to the same value at once. It accepts the same three value kinds — a fixed number, 'fill', or 'hug':

<Rect size={200} fill="royalblue" />
<Rect size="fill" fill="royalblue" />

An explicit width or height alongside size wins on that axis:

<Rect size={200} width={100} fill="royalblue" /> {/* width: 100, height: 200 */}

size works everywhere width/height do — imperative .set() and animated .to():

box().set({ size: 250 });
yield* box().to({ size: 400 }, 0.8, easeOut);

Animating size

Fixed sizes animate smoothly with .to(). Switching between fixed and 'fill'/'hug' also cross-fades:

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

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

  stage.add(<Rect ref={box} width={100} height={100} fill="royalblue" />);

  // Grow
  yield* box().to({ width: 400, height: 200 }, 0.8, easeOut);

  // Shrink back
  yield* box().to({ width: 100, height: 100 }, 0.6);
});

Measuring layout size

After layout runs, you can read the resolved pixel size from a node:

const w = box().measuredWidth;
const h = box().measuredHeight;

These are reactive: reads inside callbacks track changes automatically.