Flutter Layout Widgets: The Complete Guide With Code Examples

Coding Liquids blog cover featuring Sagnik Bhattacharya for the complete Flutter layout widgets guide, with Container, Row, Column, Stack, Expanded, and Wrap layout visuals.
Coding Liquids blog cover featuring Sagnik Bhattacharya for the complete Flutter layout widgets guide.

Almost every layout you will ever build in Flutter is made from a small set of layout widgets arranged in different ways. Once you understand Container, Row, Column, Stack, Expanded, and a handful of others — and the single rule that governs how they size each other — building UI stops feeling like guesswork. This guide walks through every layout widget that matters, with runnable code for each one, and the mental model that ties them together.

The Complete Flutter Guide course thumbnail

The Complete Flutter Guide: Build Android, iOS and Web apps

Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.

Enrol now

This is a reference you can come back to. It is organised the same way Flutter organises layout itself: first the constraints rule, then single-child layout widgets, then multi-child layout widgets, then the responsive helpers, and finally a complete worked screen that combines them. If you are brand new to Flutter, read it top to bottom once; if you are already shipping apps, jump to the widget you need.

Follow me on Instagram@sagnikteaches

Every code block here is a complete, paste-ready widget. Drop any of them into a Scaffold body in a fresh Flutter project to see the result. The examples target a current stable Flutter and Material 3, but the layout widgets themselves have been stable for years, so the concepts apply whatever version you are on.

Connect on LinkedInSagnik Bhattacharya

One more thing before we start: if you keep hitting the yellow-and-black overflow stripe, that is a layout-constraints problem, and this guide explains exactly why it happens. There is also a dedicated deep dive on fixing RenderFlex overflow linked at the end.

Subscribe on YouTube@codingliquids

The one rule that explains all Flutter layout

Before any specific widget, learn this sentence, because it explains every layout you will ever debug:

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

  • Constraints go down. A parent widget gives each child a BoxConstraints — a minimum width, maximum width, minimum height, and maximum height.
  • Sizes go up. Each child picks its own size within those constraints and reports that size back to the parent.
  • The parent sets the position. The parent decides where, relative to itself, to place the child.

Constraints come in three flavours, and most confusion disappears once you can tell them apart:

  • Tight constraints force an exact size (min equals max). A child given tight constraints has no choice — it becomes that size.
  • Loose constraints set a maximum but a minimum of zero. The child can be any size up to the maximum.
  • Unbounded constraints have an infinite maximum. This is what scrollable widgets give their children along the scroll axis, and it is the source of most "unbounded height/width" errors.

Quick examples of the rule in action: a Center receives the screen size, loosens the constraints, lets its child be its natural size, and positions it in the middle. A bare Container with no child and no size expands to fill its parent. The same Container with a child shrinks to fit the child. None of that is magic — it is just the rule applied.

Single-child layout widgets

Single-child layout widgets wrap exactly one child and change how it is sized, positioned, padded, or decorated. These are the building blocks you reach for constantly.

Container — the all-in-one box

Container is the Swiss-army widget. It composes padding, margin, alignment, constraints, and a BoxDecoration into one widget. Reach for it when you want a styled box.

Container(
  width: 200,
  height: 120,
  margin: const EdgeInsets.all(16),
  padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
  alignment: Alignment.centerLeft,
  decoration: BoxDecoration(
    color: Colors.deepPurple.shade50,
    borderRadius: BorderRadius.circular(16),
    border: Border.all(color: Colors.deepPurple.shade200),
    boxShadow: const [
      BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 4)),
    ],
  ),
  child: const Text('A styled Container'),
)

Two things trip people up. First, you cannot set both color and decoration at once — put the colour inside the BoxDecoration. Second, an empty Container() with no width, height, child, or constraints will expand to fill its parent if the parent allows it, but collapse to zero if the parent gives unbounded constraints. If you only need a fixed size or a gap, prefer SizedBox — it is lighter and can be const.

Padding — inset the child

Padding does one job and does it cheaply: it insets its child by an EdgeInsets. Because it is single-purpose, it is clearer than a Container when padding is all you need.

Padding(
  padding: const EdgeInsets.all(24),
  child: Text('I have 24 logical pixels of breathing room on every side'),
)

The EdgeInsets constructors are worth memorising: EdgeInsets.all(8), EdgeInsets.symmetric(horizontal: 16, vertical: 8), EdgeInsets.only(left: 12, top: 4), and EdgeInsets.fromLTRB(8, 4, 8, 12). For layouts that must mirror in right-to-left languages, use EdgeInsetsDirectional.only(start: 12) instead of left.

Center and Align — position within available space

Center places its child in the middle of the space it is given. Align is the general form: it places the child at any Alignment, where (-1, -1) is top-left, (0, 0) is centre, and (1, 1) is bottom-right.

Align(
  alignment: Alignment.bottomRight,
  child: FloatingActionButton(
    onPressed: () {},
    child: const Icon(Icons.add),
  ),
)

Both widgets try to be as large as possible by default (so they can position the child anywhere), then size the child loosely. If you wrap a small child in Center and nothing seems centred, check whether the Center itself is constrained to the child's size by a parent.

SizedBox — fixed size and spacing

SizedBox forces a specific width and/or height onto its child, or — with no child — acts as a fixed gap. It is the idiomatic way to space widgets apart inside a Row or Column.

Column(
  children: const [
    Text('Title'),
    SizedBox(height: 8),        // vertical gap
    Text('Subtitle'),
    SizedBox(height: 24),
    Text('Body text down here'),
  ],
)

Handy variants: SizedBox.expand fills its parent, SizedBox.shrink takes the least space possible, and SizedBox.square(dimension: 48) makes an equal-sided box. Because SizedBox is so cheap, use it for gaps rather than wrapping things in padded Containers.

The Complete Flutter Guide course thumbnail

Build real Flutter apps, not just demos

The Complete Flutter Guide takes you from widgets like these to shipping Android, iOS and web apps with state management, Firebase, and animations.

Enrol now

ConstrainedBox, UnconstrainedBox, and LimitedBox

ConstrainedBox adds extra constraints on top of what the parent already imposed. It is the right tool when a child should never grow past a maximum or shrink below a minimum.

ConstrainedBox(
  constraints: const BoxConstraints(
    minWidth: 120,
    maxWidth: 320,
    minHeight: 48,
  ),
  child: const Text('I stay between 120 and 320 px wide'),
)

UnconstrainedBox does the opposite: it lets its child be its natural size even if the parent tried to constrain it (useful, but a common cause of overflow if the child is bigger than the screen). LimitedBox only applies its limits when the incoming constraint is unbounded — it is mainly used to give a sensible maximum to children of scrolling widgets.

AspectRatio, FractionallySizedBox, and FittedBox

AspectRatio sizes its child to a width-to-height ratio. Give it an aspectRatio of 16 / 9 and it will pick the largest size with that ratio that fits the constraints.

AspectRatio(
  aspectRatio: 16 / 9,
  child: Container(color: Colors.black12, child: const Center(child: Text('16:9'))),
)

FractionallySizedBox sizes the child as a fraction of the available space — widthFactor: 0.8 makes the child 80% as wide as its parent. FittedBox scales and positions its child to fit inside the available space, which is the simplest way to stop a long line of text or a large logo from overflowing:

FittedBox(
  fit: BoxFit.scaleDown,
  child: Text('This text shrinks instead of overflowing'),
)

IntrinsicHeight, IntrinsicWidth, and SafeArea

IntrinsicHeight and IntrinsicWidth force children to share a common height or width based on their natural sizes — for example, making two cards in a Row equal height regardless of content. They are convenient but relatively expensive, so use them only where a cheaper layout will not do.

IntrinsicHeight(
  child: Row(
    crossAxisAlignment: CrossAxisAlignment.stretch,
    children: const [
      Expanded(child: Card(child: Padding(padding: EdgeInsets.all(12), child: Text('Short')))),
      Expanded(child: Card(child: Padding(padding: EdgeInsets.all(12), child: Text('Much longer text that forces a taller card and now both match')))),
    ],
  ),
)

SafeArea insets its child away from notches, status bars, and rounded screen corners. Wrap the top-level content of a screen in SafeArea so nothing important hides under the system UI.

Multi-child layout widgets

Multi-child layout widgets arrange a list of children. These are where most real screens are built.

Row and Column — the workhorses

Row lays children out horizontally; Column lays them out vertically. Both are Flex widgets, so they share the same alignment properties. The two you will set most often are mainAxisAlignment (along the layout direction) and crossAxisAlignment (perpendicular to it).

Column(
  mainAxisAlignment: MainAxisAlignment.center,   // vertical: centre the group
  crossAxisAlignment: CrossAxisAlignment.start,  // horizontal: left-align each child
  children: const [
    Text('Heading', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
    SizedBox(height: 8),
    Text('A subtitle on the next line'),
  ],
)

Key options for mainAxisAlignment are start, center, end, spaceBetween, spaceAround, and spaceEvenly. For crossAxisAlignment they are start, center, end, and stretch. Set mainAxisSize: MainAxisSize.min when you want the Row or Column to be only as big as its children rather than filling the axis.

Expanded, Flexible, and Spacer — sharing space

Inside a Row or Column, Expanded and Flexible divide the leftover space among children. Expanded forces its child to fill its share; Flexible lets the child be smaller than its share. Both accept a flex factor to control the ratio.

Row(
  children: [
    Expanded(flex: 2, child: Container(height: 60, color: Colors.red)),
    Expanded(flex: 1, child: Container(height: 60, color: Colors.green)),
    const SizedBox(width: 8),
    Flexible(child: Container(height: 60, width: 200, color: Colors.blue)),
  ],
)

Here the red box takes two-thirds and the green box one-third of the flexible space, while the blue box (wrapped in Flexible) shrinks if there is not enough room. Spacer is a shorthand for an Expanded empty gap — perfect for pushing one child to the far end of a Row:

Row(
  children: const [
    Text('Left'),
    Spacer(),       // eats all remaining space
    Text('Right'),
  ],
)

If you see "RenderFlex overflowed", it almost always means a child of a Row or Column wanted more space than was available and was not wrapped in Expanded or Flexible. The RenderFlex overflow deep dive covers every fix in detail.

Stack and Positioned — layering widgets

Stack places children on top of one another, painting later children above earlier ones. Combine it with Positioned to pin a child to an edge or corner — ideal for badges, overlays, and floating controls.

Stack(
  children: [
    Container(height: 160, width: double.infinity, color: Colors.indigo),
    const Positioned(
      bottom: 12,
      left: 16,
      child: Text('Caption over the image',
          style: TextStyle(color: Colors.white, fontSize: 18)),
    ),
    Positioned(
      top: 8,
      right: 8,
      child: Container(
        padding: const EdgeInsets.all(6),
        decoration: const BoxDecoration(color: Colors.amber, shape: BoxShape.circle),
        child: const Text('3'),
      ),
    ),
  ],
)

Non-positioned children in a Stack are aligned by the stack's alignment property (default top-left). Use the fit property and StackFit.expand when you want non-positioned children to fill the stack.

The Complete Flutter Guide course thumbnail

Go from widgets to a finished app

Layout is step one. The Complete Flutter Guide shows you how to wire these widgets into real, production-ready Android, iOS and web apps.

Enrol now

Wrap — rows and columns that break onto new lines

A Row overflows when its children do not fit. Wrap instead moves overflowing children onto the next line (or column). It is the right tool for tag lists, chip groups, and filter pills.

Wrap(
  spacing: 8,      // gap between chips on a line
  runSpacing: 8,   // gap between lines
  children: const [
    Chip(label: Text('Flutter')),
    Chip(label: Text('Dart')),
    Chip(label: Text('Layout')),
    Chip(label: Text('Widgets')),
    Chip(label: Text('Cross-platform')),
  ],
)

Table and the scrolling widgets

Table lays children out in a grid of rows and columns with shared column widths — useful for genuinely tabular data where columns must align. For long or dynamic lists, you will use the scrolling widgets: ListView, GridView, SingleChildScrollView, and the slivers. They deserve their own treatment because they introduce unbounded constraints along the scroll axis, which is a whole category of layout behaviour. See the dedicated scrolling guide linked under Further reads.

// A simple, fixed list. For long or dynamic lists use ListView.builder.
ListView(
  children: const [
    ListTile(leading: Icon(Icons.home), title: Text('Home')),
    ListTile(leading: Icon(Icons.search), title: Text('Search')),
    ListTile(leading: Icon(Icons.settings), title: Text('Settings')),
  ],
)

Responsive layout helpers

Three widgets let your layout react to the space and device it is running on, which is essential when the same Flutter app ships to phones, tablets, desktop, and web.

LayoutBuilder — build based on the constraints you receive

LayoutBuilder gives you the BoxConstraints from the parent at build time, so you can choose a layout based on the available width rather than the whole screen size.

LayoutBuilder(
  builder: (context, constraints) {
    if (constraints.maxWidth >= 600) {
      // Wide: two columns side by side
      return Row(
        children: const [
          Expanded(child: _Sidebar()),
          Expanded(flex: 2, child: _Content()),
        ],
      );
    }
    // Narrow: stack them
    return Column(children: const [_Content(), _Sidebar()]);
  },
)

MediaQuery and OrientationBuilder

MediaQuery.of(context) exposes the screen size, padding (notches), text scale factor, and platform brightness. Use MediaQuery.sizeOf(context) to read just the size efficiently. OrientationBuilder rebuilds when the device rotates so you can switch between portrait and landscape layouts. For a full treatment of multi-device design, see the responsive UI guide under Further reads.

final width = MediaQuery.sizeOf(context).width;
final columns = width >= 900 ? 4 : width >= 600 ? 3 : 2;
// feed columns into a GridView.count(crossAxisCount: columns, ...)

A complete worked example: a profile card

Here is a single screen that combines many of the widgets above — SafeArea, Padding, Column, Row, Stack, Positioned, Expanded, SizedBox, and Container decoration. Paste it into a fresh app to see how the pieces fit together.

import 'package:flutter/material.dart';

void main() => runApp(const MaterialApp(home: ProfileScreen()));

class ProfileScreen extends StatelessWidget {
  const ProfileScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Profile')),
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(20),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // Avatar with an online badge using Stack + Positioned
              Center(
                child: Stack(
                  children: [
                    const CircleAvatar(radius: 44, child: Icon(Icons.person, size: 44)),
                    Positioned(
                      right: 2,
                      bottom: 2,
                      child: Container(
                        width: 18,
                        height: 18,
                        decoration: BoxDecoration(
                          color: Colors.green,
                          shape: BoxShape.circle,
                          border: Border.all(color: Colors.white, width: 2),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
              const SizedBox(height: 16),
              const Center(
                child: Text('Sagnik Bhattacharya',
                    style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
              ),
              const SizedBox(height: 4),
              const Center(child: Text('Flutter developer & instructor')),
              const SizedBox(height: 24),

              // A stats row: three Expanded cells share the width evenly
              Row(
                children: const [
                  Expanded(child: _Stat(label: 'Posts', value: '128')),
                  Expanded(child: _Stat(label: 'Courses', value: '6')),
                  Expanded(child: _Stat(label: 'Students', value: '30k')),
                ],
              ),
              const SizedBox(height: 24),

              // A full-width primary action
              SizedBox(
                width: double.infinity,
                child: FilledButton(onPressed: () {}, child: const Text('Follow')),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _Stat extends StatelessWidget {
  final String label;
  final String value;
  const _Stat({required this.label, required this.value});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(value, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
        const SizedBox(height: 2),
        Text(label, style: const TextStyle(color: Colors.black54)),
      ],
    );
  }
}

Notice how no single widget does everything. The avatar badge is a Stack, the stats are three Expanded cells in a Row, the spacing is SizedBox, and the full-width button is a SizedBox with width: double.infinity. That composition style is the whole point of Flutter layout.

Common layout mistakes (and the fix)

  • "RenderFlex overflowed" in a Row/Column. A child wanted more space than was available. Wrap the flexible child in Expanded or Flexible, or let text truncate with overflow: TextOverflow.ellipsis.
  • "Unbounded height/width". You put a Column or ListView inside another scrollable without bounding it. Give it a height with SizedBox, use Expanded inside a parent Column, or set shrinkWrap: true on the inner list.
  • Setting both color and decoration on a Container. Move the colour into the BoxDecoration.
  • Expanded used outside a Row/Column. Expanded and Flexible only work as direct children of a Flex (Row, Column). Elsewhere you get an assertion error.
  • Empty Container() collapsing or expanding unexpectedly. Be explicit: use SizedBox for a known size, or give the container constraints.
  • Overusing IntrinsicHeight. It forces an extra measurement pass. Reach for CrossAxisAlignment.stretch or a fixed height first.

Which layout widget should I use?

  • Style a box (colour, border, radius, shadow) → Container with BoxDecoration.
  • Just add space or a fixed size → SizedBox.
  • Inset a child → Padding.
  • Place a child in a position → Center or Align.
  • Lay widgets in a line → Row (horizontal) or Column (vertical).
  • Share leftover space → Expanded (fill) or Flexible (shrink-to-fit), with Spacer to push things apart.
  • Overlap or pin children → Stack + Positioned.
  • Let items wrap to new lines → Wrap.
  • Cap or set a size range → ConstrainedBox, AspectRatio, or FractionallySizedBox.
  • React to available space → LayoutBuilder or MediaQuery.
  • Scroll a long list → ListView / GridView (see the scrolling guide).

Frequently asked questions

What are layout widgets in Flutter?

Layout widgets position and size other widgets instead of painting content. They are split into single-child widgets (Container, Padding, Center, Align, SizedBox, ConstrainedBox, AspectRatio, FittedBox) and multi-child widgets (Row, Column, Stack, Wrap, ListView, GridView).

What is the difference between Container and SizedBox?

SizedBox only sets a size or gap and can be const, so it is lighter. Container bundles padding, margin, alignment, decoration, and constraints. Use SizedBox for spacing and fixed sizes, Container when you also need decoration or padding.

How does Flutter decide a widget's size?

By the rule "constraints go down, sizes go up, the parent sets the position." A parent passes min/max width and height to each child; the child picks a size within those bounds and reports back; the parent positions it. Most layout bugs are a widget getting unbounded constraints or being forced to an impossible size.

What is the difference between Expanded and Flexible?

Both share leftover space in a Row or Column. Expanded forces the child to fill its share (tight fit). Flexible lets the child be smaller than its share (loose fit). Use Expanded to fill, Flexible to shrink-to-fit within a limit.

Further reads

Keep going with the tutorials that build directly on these layout foundations:

Sources: Flutter documentation — Layout widgets, Understanding constraints, and the Widget catalog (docs.flutter.dev). Verified against current stable Flutter and Material 3.