← Back to blog
Flutter Fundamentals · Part 6 of 9
July 20, 202612 min read

Flutter Layouts II: Stack, Positioned & the Constraints Model

FlutterDart

Flutter Layouts II: Stack & Constraints

This is Part 6 of the Flutter Fundamentals series. Part 5 placed widgets side by side. Now two things: how to overlap widgets (Stack/Positioned), and — the big one — the constraints model that governs all Flutter layout. The constraints model is the single concept that turns Flutter layout from "mysterious" ("why is my Container huge?") into "predictable." We'll end the guessing here.


Stack: overlapping widgets

Row/Column lay children out in a line. Stack layers them on top of each other, like sheets of paper in a pile — later children paint over earlier ones.

Stack(
  children: [
    Container(width: 200, height: 200, color: Colors.blue), // bottom
    Container(width: 100, height: 100, color: Colors.red),  // on top
    const Text('On top of everything'),                     // topmost
  ],
)

Use a Stack for badges on icons, text over an image, a floating button over content, overlays/scrims — anything where widgets occupy the same space.

Positioning within a Stack: Positioned

By default, non-positioned children are placed according to the Stack's alignment (top-start by default). To pin a child to specific edges, wrap it in Positioned:

Stack(
  children: [
    const CircleAvatar(radius: 40, child: Icon(Icons.person)),
    Positioned(
      right: 0,
      top: 0,
      child: Container(
        padding: const EdgeInsets.all(4),
        decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle),
        child: const Text('3', style: TextStyle(color: Colors.white)),
      ),
    ),
  ],
)

Positioned takes top, right, bottom, left, width, height. Specify any combination:

  • Give top + left → pin to that corner.
  • Give left + right (no width) → stretch horizontally between them.
  • A child not wrapped in Positioned is laid out by the Stack's alignment.

Positioned only works as a direct child of a Stack. It's one of a few "parent data" widgets (like Expanded inside a Row) that only mean something inside their specific parent.

Sizing a Stack

A Stack sizes itself to its largest non-positioned child (subject to its constraints). Positioned children don't influence the Stack's size. To make a Stack fill its parent regardless, use fit: StackFit.expand or wrap in a SizedBox.expand.


The constraints model: the rule that explains everything

Now the centerpiece. Every layout question in Flutter — "why is this too big / too small / ignored?" — is answered by one sentence, straight from the official docs:

Constraints go down. Sizes go up. Parent sets position.

Read it three times. It describes how every widget gets laid out, in three steps:

  1. Constraints go down. A parent passes each child a BoxConstraints — a min/max width and min/max height — saying "you must be somewhere in this range."
  2. Sizes go up. Each child picks its own size within those constraints and reports it back up to the parent.
  3. Parent sets position. The parent, now knowing the child's size, decides where to place it.

It's a single downward-then-upward pass over the tree — which is why Flutter layout is O(n) (fast): each widget is visited essentially once.

The negotiation, dramatized

From the docs, the conversation between a widget and its child:

Widget: "Hey parent, what are my constraints?" Parent: "You must be from 0 to 300 wide, and 0 to 85 tall." Widget: "I want 5 px of padding, so my child can be at most 290 × 75." Widget (to child): "You must be from 0 to 290 wide, and 0 to 75 tall." Child: "OK, I'll be 290 × 20." (parent positions the child, then reports its own size upward)

That back-and-forth is literally how layout runs. Once you can hear this conversation in your head, layout stops being magic.


Tight vs loose constraints

A BoxConstraints can be tight or loose, and this distinction explains most surprises:

  • Tight — min == max. The parent says "you must be exactly this size." No choice. (Example: the screen forces your root widget to be exactly the screen's size.)
  • Loose — min is 0. "You can be anywhere from nothing up to this max — your call." (Example: Center says "be any size you like, up to my bounds.")

This is the key to the most-asked Flutter layout question of all time...


The famous gotcha: "why does Container(width: 100) fill the screen?"

// You expect a 100×100 red box. You get a full-screen red box. Why?!
Scaffold(
  body: Container(width: 100, height: 100, color: Colors.red),
)

Why: the Scaffold body gives the Container tight constraints equal to the whole screen — "you must be screen-sized." A Container with a child of its own size obeys; but the rule is that a tightly-constrained widget can't pick its own size — the parent already forced it. The width: 100 is requested, but the tight constraint overrides it.

The fix — introduce a widget that loosens the constraints, like Center (or Align):

// Now you get a real 100×100 box.
Scaffold(
  body: Center(
    child: Container(width: 100, height: 100, color: Colors.red),
  ),
)

Center takes the tight (full-screen) constraints, but passes loose constraints down to its child ("be up to full-screen, but you choose"). Now the Container is free to be 100×100, and Center positions it in the middle. This single pattern — wrap in Center/Align to escape tight constraints — solves a huge fraction of "my size is being ignored" bugs.


More consequences worth internalizing

The official docs distill the limitations that fall out of the one-pass model:

  1. A widget can only be as big as its parent allows. It can't be any size it wants — it must obey the incoming constraints.
  2. A widget doesn't choose its own position — its parent does. (So you can't "move yourself"; you ask a parent like Align/Positioned/Padding to move you.)
  3. Size & position depend on the whole tree. You can't determine a widget's size in isolation; it's the result of constraints flowing down from every ancestor.

A few widgets and how they behave under constraints:

  • Tries to be as big as possible: Center (when given a child... it centers, but itself fills), ListView, an empty Container.
  • Tries to match its child: Transform, Opacity, SizedBox.shrink.
  • Tries to be a specific size: Image, Text, SizedBox(width:.., height:..).

Why an empty Container fills its parent but a Container with a small child shrinks to it: an unconstrained Container with no size and no child decides to be as big as possible; give it a child (and loose constraints) and it sizes to the child. This is a design choice by the Container authors — when in doubt, the answer is "read what constraints the parent gives, then read what that specific widget does with them."


Tools for bending constraints

When the default constraints aren't what you want, these widgets adjust them:

| Widget | Effect | | --- | --- | | Center / Align | loosen constraints + position the child | | SizedBox(width, height) | impose a specific size (tight, if unconstrained) | | ConstrainedBox | add extra min/max limits (can only tighten, never loosen what it's given) | | UnconstrainedBox | let the child be its natural size (warns on overflow) | | FittedBox | scale the child to fit the available space | | LayoutBuilder | read the incoming constraints and build differently |

A subtle but important one: ConstrainedBox can only add constraints, never relax the ones it receives. If its parent already forces a tight size, the ConstrainedBox's own min/max are ignored — which is exactly Example 9 in the official "Understanding constraints" doc. Wrap it in a Center first to loosen, and its constraints take effect.

// Constraints ignored — the screen forces tight constraints through.
ConstrainedBox(
  constraints: const BoxConstraints(minWidth: 70, minHeight: 70),
  child: Container(color: Colors.red, width: 10, height: 10),
)

// Now respected — Center loosens first, so the min applies (box becomes 70×70).
Center(
  child: ConstrainedBox(
    constraints: const BoxConstraints(minWidth: 70, minHeight: 70),
    child: Container(color: Colors.red, width: 10, height: 10),
  ),
)

LayoutBuilder: react to the constraints you're given

When you need to adapt to the available space (responsive layouts), LayoutBuilder hands you the incoming constraints so you can branch:

LayoutBuilder(
  builder: (context, constraints) {
    if (constraints.maxWidth < 600) {
      return const OneColumnLayout(); // phone
    }
    return const TwoColumnLayout();   // tablet/desktop
  },
)

This is the constraints model exposed directly to you — "here's what the parent allows; build accordingly."


Debugging layout

When a layout misbehaves, your best friends are:

  • Flutter DevTools → Layout Explorer — visualizes the constraints each widget receives and the size it chose. It literally shows "constraints go down, sizes go up" for your tree.
  • debugPaintSizeEnabled = true — paints box outlines so you can see where things actually are.
  • The mental script: "What constraints does the parent give this widget? What does this particular widget do with them?" Almost every layout bug answers to those two questions.

Practice Challenges

Challenge 1 — Badge on an avatar. Use a Stack + Positioned to put a small red dot at the top-right of a CircleAvatar.

Show solution
Stack(
  children: [
    const CircleAvatar(radius: 30, child: Icon(Icons.person)),
    Positioned(
      right: 0, top: 0,
      child: Container(width: 14, height: 14,
        decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle)),
    ),
  ],
)

Positioned pins the dot to the corner; it must be a direct child of the Stack.

Challenge 2 — Explain the full-screen box. A Container(width: 100, height: 100) as a Scaffold body fills the screen. Explain why and fix it.

Show solution

The body gives the Container tight constraints equal to the screen, and a tightly-constrained widget can't choose its own size — so width: 100 is overridden and it fills the screen. Fix by wrapping in Center (or Align), which passes loose constraints down, letting the Container actually be 100×100:

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

Challenge 3 — Recite the rule. State the three-part layout rule and what each part means.

Show solution

"Constraints go down. Sizes go up. Parent sets position." (1) A parent passes each child min/max width & height constraints. (2) The child chooses its size within those constraints and reports it upward. (3) The parent, knowing the child's size, positions it. One down-then-up pass → O(n).

Challenge 4 — Why are these constraints ignored? A ConstrainedBox(minWidth: 70) around a tiny Container doesn't enforce 70px when used directly as a body. Why, and how do you make it work?

Show solution

ConstrainedBox can only add constraints, not relax incoming ones. As a body it receives tight (full-screen) constraints, which it passes straight through, so its own minWidth has no room to act. Wrap it in a Center first to loosen the constraints; then the minWidth: 70 applies and the box becomes at least 70px wide.

Challenge 5 — Responsive switch. Show a two-column layout on wide screens and one column on narrow, using the constraints.

Show solution
LayoutBuilder(builder: (context, constraints) {
  return constraints.maxWidth < 600
      ? const OneColumnLayout()
      : const TwoColumnLayout();
});

LayoutBuilder exposes the incoming constraints so you can branch on available width.


Questions to test yourself

Q1 (basic). What does a Stack do, and how do you position a child at a specific corner?

Show answer

A Stack layers its children on top of each other (later children paint over earlier ones). To pin a child to specific edges, wrap it in a Positioned (giving top/right/bottom/left); Positioned must be a direct child of the Stack. Non-positioned children are placed by the Stack's alignment.

Q2 (basic). State Flutter's three-part layout rule.

Show answer

"Constraints go down. Sizes go up. Parent sets position." Constraints (min/max width & height) flow from parent to child; each child picks a size within them and reports it back up; the parent then positions the child.

Q3 (intermediate). What's the difference between tight and loose constraints?

Show answer

Tight: min == max — the parent forces an exact size, the child has no choice (e.g. the screen forcing the root widget's size). Loose: min is 0 — the child may be any size up to the max (e.g. Center letting its child be any size up to the screen). Loose constraints let a child choose its own size; tight ones don't.

Q4 (intermediate). Why does a Container(width: 100) fill the screen when used as a Scaffold body, and what's the fix?

Show answer

The body gives the Container tight constraints equal to the screen, and a tightly-constrained widget can't pick its own size — so its width: 100 is overridden and it fills the screen. Wrap it in Center (or Align), which passes loose constraints down, letting the Container actually take 100×100 and be positioned in the middle.

Q5 (intermediate). Can a widget decide its own position? Who can move it?

Show answer

No — a widget cannot choose its own position; its parent sets it. To move a widget, you wrap it in a parent that positions children: Align/Center, Padding, Positioned (inside a Stack), or use the flex alignment of a Row/Column.

Q6 (advanced). Why does ConstrainedBox sometimes appear to ignore its constraints, and why is the constraints model O(n)?

Show answer

ConstrainedBox can only add (tighten) constraints — it can't loosen what its parent gives it. If the parent imposes tight constraints, ConstrainedBox passes them through and its own min/max have no effect (wrap in Center to loosen first). The model is O(n) because layout is a single pass: constraints flow down once and sizes flow up once, so each RenderObject is visited essentially once — no expensive back-tracking or multiple passes.


Wrapping up

You now understand why Flutter layouts behave the way they do:

  • Stack layers widgets; Positioned (a direct Stack child) pins them to edges; the Stack sizes to its largest non-positioned child.
  • The entire layout system is one rule: constraints go down, sizes go up, parent sets position — a single O(n) pass.
  • Tight constraints force a size; loose ones let the child choose. Most "my size is ignored" bugs are tight constraints — wrap in Center/Align to loosen.
  • A widget can't pick its own position; a parent does. ConstrainedBox can only add constraints.
  • Use LayoutBuilder to adapt to available space and DevTools' Layout Explorer to debug.

You can now build and arrange real screens. But so far we've been editing code and... how do those edits show up so fast? In Part 7 we look under the hood of the feature that makes Flutter development feel magical: hot reload vs hot restart — what actually happens.