Skip to content

Constraints & Layout

If you remember nothing else about Flutter layout, remember this:

Constraints go down. Sizes go up. The parent sets the position.

Layout is a single pass down the tree and back up. A parent passes constraints to each child — a BoxConstraints with a min and max width and height. Each child must choose a size that satisfies those constraints, and reports it back up. The parent then positions the child inside itself.

flowchart TB
  parent["Parent widget"] -->|"constraints:
min/max w, min/max h"| child["Child widget"]
  child -->|"chosen size
(within constraints)"| parent
  parent -->|"offset
(where to place child)"| placed["Child placed"]
One layout pass: down with constraints, up with sizes

That is the entire algorithm. Everything else is detail about how a particular widget chooses its size and what constraints it passes on.

Center(
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
)

Reading top-down:

  1. Center receives constraints from its parent (say, the whole screen: exactly the screen size).
  2. Center passes loose constraints to its child — “you may be any size from 0 up to the screen.”
  3. Container wants to be exactly 100x100, which fits, so it sizes to 100x100 and reports up.
  4. Center is as big as the screen, and positions the 100x100 child in the middle.

Change Center to Align, Padding, or SizedBox and only step 2 changes — the constraints passed down. The rule never changes.

A constraint is tight when min equals max (the widget must be exactly that size) and loose when min is 0 (the widget may be anything up to max).

// SizedBox imposes a tight constraint — the child MUST be 50x50.
SizedBox(width: 50, height: 50, child: coloredBox)
// Center imposes a loose constraint — the child may be up to the parent size.
Center(child: coloredBox)

This is why the same widget behaves differently under different parents: Container(color: Colors.red) with no size fills its parent under tight constraints, but shrinks to nothing under loose ones. The widget did not change — the constraints did.

Some widgets pass unbounded constraints — a max of infinity — meaning “you can be as big as you want.” A vertical ListView, for example, gives its children unbounded height (the scroll direction is effectively infinite).

Put a Column inside that ListView and you get the classic error: a Column tries to be as tall as its constraints allow, but the constraint is infinite, so it cannot pick a finite height.

ListView(
children: [
Column( // ❌ RenderFlex: unbounded height
children: [ Text('a'), Text('b') ],
),
],
)

The fix is to give the child a bounded height — wrap it, use shrinkWrap, or restructure so the scrollable is the outer widget. Once you can read constraints, these errors read as sentences rather than mysteries.

What is the core rule of Flutter layout?
What is a "tight" constraint?
Why does putting a Column directly inside a vertical ListView error?
Why does the same unsized Container fill one parent but shrink under another?