Flutter layouts feel confusing until you understand one rule: everything is a widget — including the layout itself.
In most frameworks, flutter layout is a separate concern from UI components. In Flutter, there’s no such separation. A Row is a widget. A Column is a widget. Even spacing and padding are widgets. Once that clicks, the whole system becomes predictable — because you’re just nesting widgets inside other widgets, all the way down.
This guide covers the four layout widgets you’ll use in almost every Flutter screen: Row, Column, Flex, and Expanded. Every section has a visual diagram, a complete copy-paste code example, and a clear explanation of what each property actually does. By the end you’ll know how to build any common UI structure — and how to fix the overflow errors that trip up every beginner.
Table of Contents
1. The Core Mental Model: Main Axis vs Cross Axis
Before writing any layout code, you need one concept locked in: every flex layout widget has a main axis (the direction it places children) and a cross axis (perpendicular to it). These two axes determine how all the alignment properties behave.
ROW
┌──────────────────────────────────────────┐
│ ←────── main axis (horizontal) ──────→ │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ A │ │ B │ │ C │ ↕ cross │
│ └──────┘ └──────┘ └──────┘ │
└──────────────────────────────────────────┘
COLUMN
┌──────────────────┐
│ ┌────────────┐ │ ↕ main axis (vertical)
│ │ A │ │
│ └────────────┘ │
│ ┌────────────┐ │
│ │ B │ │
│ └────────────┘ │
│ ┌────────────┐ │
│ │ C │ │ ↔ cross axis
│ └────────────┘ │
└──────────────────┘
| Widget | Main axis | Cross axis | mainAxisAlignment controls |
crossAxisAlignment controls |
|---|---|---|---|---|
Row |
Horizontal ↔ | Vertical ↕ | Horizontal spacing between children | Vertical alignment of children |
Column |
Vertical ↕ | Horizontal ↔ | Vertical spacing between children | Horizontal alignment of children |
Every alignment decision you make will refer back to this table. When you set mainAxisAlignment on a Row, you’re controlling horizontal spacing. When you set crossAxisAlignment on a Row, you’re controlling how children sit vertically within the row’s height.
2. Row — Horizontal Layout
A Row places its children side by side horizontally. It expands to fill the full width available to it by default and sizes itself to the tallest child vertically.
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: RowExample()));
class RowExample extends StatelessWidget {
const RowExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Row Example')),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// ── spaceEvenly: equal space before, between, and after ──
const Text('spaceEvenly', style: TextStyle(fontWeight: FontWeight.bold)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: const [
Icon(Icons.home, size: 40),
Icon(Icons.favorite, size: 40),
Icon(Icons.settings, size: 40),
],
),
const Divider(height: 32),
// ── spaceBetween: space only between items, flush to edges ──
const Text('spaceBetween', style: TextStyle(fontWeight: FontWeight.bold)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Icon(Icons.star, size: 40),
Icon(Icons.share, size: 40),
Icon(Icons.delete, size: 40),
],
),
const Divider(height: 32),
// ── center: all children grouped at center ──
const Text('center', style: TextStyle(fontWeight: FontWeight.bold)),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.thumb_up, size: 40),
SizedBox(width: 16),
Icon(Icons.thumb_down, size: 40),
],
),
],
),
);
}
}
Row, SizedBox(width: 16) is cleaner than relying on mainAxisAlignment alone. Use SizedBox(height: N) for gaps in a Column.
3. Column — Vertical Layout
A Column stacks its children top-to-bottom along the vertical axis. It works identically to Row — the only difference is the direction of the main axis.
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: ColumnExample()));
class ColumnExample extends StatelessWidget {
const ColumnExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Column Example')),
body: Column(
// center: children grouped in the middle of the vertical space
mainAxisAlignment: MainAxisAlignment.center,
// center: children aligned to the middle horizontally
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.person, size: 72, color: Colors.blueGrey),
const SizedBox(height: 12),
const Text(
'Flutter Dev',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
const Text(
'Building beautiful apps',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {},
child: const Text('Follow'),
),
],
),
);
}
}
4. Alignment Reference: All mainAxisAlignment Values
This is the table you’ll want to bookmark. Both Row and Column share the same MainAxisAlignment enum — just remember to apply the mental model from Section 1 to know which physical direction each value affects.
| Value | Visual result | When to use it |
|---|---|---|
start (default) |
[A][B][C]· · · · · |
Children packed at the start — default behavior |
end |
· · · · ·[A][B][C] |
Right-align / bottom-align children |
center |
· · [A][B][C] · · |
Center all children as a group |
spaceBetween |
[A]· · · [B]· · · [C] |
Space only between items — flush to edges |
spaceAround |
· [A]· · [B]· · [C]· |
Half-space at edges, full space between |
spaceEvenly |
· [A]· · [B]· · [C]· |
Equal space before, between, and after all items |
| CrossAxisAlignment value | Effect in a Row | Effect in a Column |
|---|---|---|
center (default) |
Children vertically centered within row height | Children horizontally centered |
start |
Children aligned to top of row | Children aligned to left edge |
end |
Children aligned to bottom of row | Children aligned to right edge |
stretch |
Children stretched to row’s full height | Children stretched to column’s full width |
baseline |
Text baselines aligned (requires textBaseline) |
N/A |
5. Expanded — Fill Available Space
Expanded is used inside a Row, Column, or Flex to make one child take up all remaining space on the main axis after the fixed-size siblings have been measured. It’s one of the most useful widgets in Flutter and solves a huge range of common layout problems.
Expanded in a Column — fixed header, stretching body
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: ExpandedColumnExample()));
class ExpandedColumnExample extends StatelessWidget {
const ExpandedColumnExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Expanded in Column')),
body: Column(
children: [
// Fixed-height header — takes exactly 80px
Container(
height: 80,
color: Colors.orange,
child: const Center(
child: Text('Fixed Header (80px)',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
),
),
// Expanded — takes ALL remaining height below the header
Expanded(
child: Container(
color: Colors.purple.shade100,
child: const Center(
child: Text('Fills all remaining screen height',
style: TextStyle(fontSize: 16)),
),
),
),
],
),
);
}
}
Screen height = 800px
AppBar = 56px
Fixed header = 80px
─────────────────────
Remaining = 664px ← Expanded fills exactly this
Expanded in a Row — input field + button
One of the most common patterns in real apps — a text field takes all available width, and a button sits alongside it at its natural size:
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: SearchBarExample()));
class SearchBarExample extends StatelessWidget {
const SearchBarExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Search Bar')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// TextField takes all remaining width
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: 'Search...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
),
),
),
const SizedBox(width: 12),
// Button keeps its natural (intrinsic) width
ElevatedButton(
onPressed: () {},
child: const Text('Search'),
),
],
),
),
);
}
}
6. Expanded with flex: — Proportional Space Splitting
When multiple siblings are all Expanded, Flutter splits the remaining space between them proportionally based on their flex value. The default flex is 1. Setting flex: 2 gives that child twice as much space as a sibling with flex: 1.
Row with flex ratios 1 : 2 : 1
Total parts = 1 + 2 + 1 = 4
[ Red 1/4 ][ Blue 2/4 ][ Green 1/4 ]
│← 25% →│← 50% →│← 25% →│
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: FlexRatioExample()));
class FlexRatioExample extends StatelessWidget {
const FlexRatioExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Flex Ratios')),
body: Column(
children: [
// ── Row: 1 : 2 : 1 ratio ──────────────────────────────
const Padding(
padding: EdgeInsets.all(12),
child: Text('Row — flex 1 : 2 : 1',
style: TextStyle(fontWeight: FontWeight.bold)),
),
Row(
children: [
Expanded(
// flex: 1 is the default — no need to write it
child: Container(
height: 80,
color: Colors.red,
child: const Center(
child: Text('1x', style: TextStyle(color: Colors.white)),
),
),
),
Expanded(
flex: 2, // ← gets twice the space of its siblings
child: Container(
height: 80,
color: Colors.blue,
child: const Center(
child: Text('2x', style: TextStyle(color: Colors.white)),
),
),
),
Expanded(
child: Container(
height: 80,
color: Colors.green,
child: const Center(
child: Text('1x', style: TextStyle(color: Colors.white)),
),
),
),
],
),
const SizedBox(height: 24),
// ── Column: 1 : 3 ratio ───────────────────────────────
const Padding(
padding: EdgeInsets.all(12),
child: Text('Column — flex 1 : 3',
style: TextStyle(fontWeight: FontWeight.bold)),
),
Expanded(
child: Column(
children: [
Expanded(
child: Container(
color: Colors.amber,
child: const Center(child: Text('1x — 25% height')),
),
),
Expanded(
flex: 3,
child: Container(
color: Colors.teal,
child: const Center(
child: Text('3x — 75% height',
style: TextStyle(color: Colors.white)),
),
),
),
],
),
),
],
),
);
}
}
7. Flex — The Widget Behind Row and Column
Row and Column are both specialised versions of Flex. The only difference is that Flex requires you to explicitly pass a direction — Axis.horizontal (same as Row) or Axis.vertical (same as Column). Everything else — mainAxisAlignment, crossAxisAlignment, Expanded children — works identically.
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: FlexExample()));
class FlexExample extends StatelessWidget {
const FlexExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Flex Widget')),
body: Column(
children: [
// Flex with Axis.horizontal = same as Row
Flex(
direction: Axis.horizontal,
children: [
Expanded(
child: Container(
height: 80,
color: Colors.teal,
child: const Center(
child: Text('Flex horizontal 1x',
style: TextStyle(color: Colors.white)),
),
),
),
Expanded(
flex: 3,
child: Container(
height: 80,
color: Colors.amber,
child: const Center(child: Text('Flex horizontal 3x')),
),
),
],
),
const SizedBox(height: 24),
// The equivalent using Row — identical output
Row(
children: [
Expanded(
child: Container(
height: 80,
color: Colors.teal,
child: const Center(
child: Text('Row 1x',
style: TextStyle(color: Colors.white)),
),
),
),
Expanded(
flex: 3,
child: Container(
height: 80,
color: Colors.amber,
child: const Center(child: Text('Row 3x')),
),
),
],
),
],
),
);
}
}
Flex when you’re building a reusable widget that accepts a direction parameter. For example, a Divider-style widget that can be horizontal or vertical depending on context. For everything else, Row and Column are clearer and more readable.
8. Real UI Examples: Combining Row, Column & Expanded
Profile Card — Row inside Card with nested Column
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: ProfileCardExample()));
class ProfileCardExample extends StatelessWidget {
const ProfileCardExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profile Card')),
backgroundColor: Colors.grey.shade100,
body: Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
// crossAxisAlignment.center keeps avatar and text
// vertically centered even if text wraps to two lines
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const CircleAvatar(
radius: 32,
backgroundColor: Colors.blueGrey,
child: Icon(Icons.person, size: 32, color: Colors.white),
),
const SizedBox(width: 16),
// Expanded prevents the text column from overflowing
Expanded(
child: Column(
// MainAxisSize.min packs the column to its content height
// instead of stretching to fill the card vertically
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'Flutter Developer',
style: TextStyle(
fontSize: 17, fontWeight: FontWeight.bold),
),
SizedBox(height: 4),
Text(
'Building beginner tutorials',
style: TextStyle(fontSize: 13, color: Colors.grey),
),
],
),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: () {},
child: const Text('Follow'),
),
],
),
),
),
),
),
);
}
}
Equal-Width Buttons — Expanded side by side
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: EqualButtonsExample()));
class EqualButtonsExample extends StatelessWidget {
const EqualButtonsExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Equal Buttons')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () {},
child: const Text('Log In'),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton(
onPressed: () {},
child: const Text('Register'),
),
),
],
),
const SizedBox(height: 16),
// 2:1 ratio — primary action gets more visual weight
Row(
children: [
Expanded(
flex: 2,
child: ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.amber,
),
child: const Text('Continue'),
),
),
const SizedBox(width: 12),
Expanded(
child: TextButton(
onPressed: () {},
child: const Text('Cancel'),
),
),
],
),
],
),
),
);
}
}
9. Overflow: What It Is and How to Fix It
When a Row or Column‘s children take up more space than the widget has available, Flutter renders the infamous yellow-and-black hatched overflow stripe along the overflowing edge. This is Flutter telling you: “Something doesn’t fit — you need to decide how to handle it.”
The overflow: three ways to fix it
// ❌ CAUSES OVERFLOW — long text pushes past the Row's right edge
Row(
children: const [
Text('This is a very very very very long text that will overflow'),
Icon(Icons.warning),
],
)
// ✅ FIX 1: Expanded + ellipsis — truncates with "..." when too long
Row(
children: const [
Expanded(
child: Text(
'This is a very very very very long text that now fits nicely',
overflow: TextOverflow.ellipsis, // shows "..." at the cut-off point
maxLines: 1,
),
),
Icon(Icons.warning),
],
)
// ✅ FIX 2: Expanded + wrap — text flows onto multiple lines
Row(
children: const [
Expanded(
child: Text(
'This long text will wrap to multiple lines instead of overflowing',
softWrap: true,
),
),
Icon(Icons.warning),
],
)
// ✅ FIX 3: Wrap widget — children wrap to a new row when they don't fit
Wrap(
spacing: 8, // horizontal gap between items
runSpacing: 4, // vertical gap between rows
children: const [
Chip(label: Text('Flutter')),
Chip(label: Text('Dart')),
Chip(label: Text('Mobile')),
Chip(label: Text('Cross-platform')),
Chip(label: Text('Open source')),
],
)
| Situation | Best fix |
|---|---|
| Single-line text overflowing in a Row | Expanded + TextOverflow.ellipsis |
| Text that should wrap onto multiple lines | Expanded with default softWrap: true |
| Multiple chips or tags that may not fit | Wrap widget instead of Row |
| Column content taller than the screen | ListView instead of Column |
10. Common Beginner Mistakes
Mistake 1: Using Expanded outside a Row, Column, or Flex
// ❌ WRONG — Expanded must have a flex parent
Container(
child: Expanded( // throws: "Expanded widgets must be placed inside a Flex widget"
child: Text('Error'),
),
)
// ✅ CORRECT — always inside Row, Column, or Flex
Row(
children: [
Expanded(child: Text('Works perfectly')),
],
)
Mistake 2: Using Column for a scrollable page
// ❌ WRONG — Column doesn't scroll. When content exceeds screen height,
// you get a RenderFlex overflow error
body: Column(
children: List.generate(30, (i) => ListTile(title: Text('Item $i'))),
)
// ✅ CORRECT — ListView handles overflow by scrolling automatically
body: ListView(
children: List.generate(30, (i) => ListTile(title: Text('Item $i'))),
)
// ✅ ALSO CORRECT — if you must use Column, wrap it in SingleChildScrollView
// (but ListView.builder is more efficient for long lists)
body: SingleChildScrollView(
child: Column(
children: List.generate(30, (i) => ListTile(title: Text('Item $i'))),
),
)
Mistake 3: Forgetting mainAxisSize: MainAxisSize.min on inner columns
// ❌ The inner Column stretches vertically to fill the Row's height,
// pushing the text block to the top or bottom unexpectedly
Row(
children: [
CircleAvatar(),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [Text('Title'), Text('Subtitle')],
),
),
],
)
// ✅ CORRECT — MainAxisSize.min makes the Column hug its content
Row(
children: [
const CircleAvatar(),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min, // ← packs tightly around content
crossAxisAlignment: CrossAxisAlignment.start,
children: const [Text('Title'), Text('Subtitle')],
),
),
],
)
Mistake 4: Nesting two Expanded widgets incorrectly
// ❌ WRONG — you cannot nest Expanded directly inside another Expanded
// without an intermediate Row/Column in between
Row(
children: [
Expanded(
child: Expanded( // throws a layout error
child: Container(),
),
),
],
)
// ✅ CORRECT — each Expanded must be a direct child of a flex widget
Row(
children: [
Expanded(
child: Column( // Column is the flex parent for the inner Expanded
children: [
Expanded(child: Container(color: Colors.red)),
Expanded(child: Container(color: Colors.blue)),
],
),
),
],
)
11. Interview Q&A
Q1: What is the difference between Row and Column?
Row lays out its children horizontally — the main axis is horizontal, the cross axis is vertical. A Column lays out its children vertically — the main axis is vertical, the cross axis is horizontal. Both accept the same alignment properties and work the same way; only the direction differs.
Q2: What does Expanded do, and where can you use it?
Expanded makes a child of a Row, Column, or Flex fill all remaining space on the main axis after fixed-size siblings have been measured. It can only be used as a direct child of one of those three flex widgets — using it anywhere else throws a runtime layout error.
Q3: What does flex: 2 mean on an Expanded widget?
flex: 1. Flutter adds up all the flex values (e.g. 1 + 2 + 1 = 4), then allocates each child its fraction of the available space (25%, 50%, 25%). The default flex value is 1.
Q4: What is Flex and how does it relate to Row and Column?
Row and Column are both subclasses of Flex. Flex is the underlying implementation — it requires an explicit direction parameter (Axis.horizontal or Axis.vertical) while Row and Column hardcode that direction. All three support the same children, alignment, and Expanded behaviour.
Q5: Why does Flutter show the yellow-and-black overflow stripe?
Expanded, adding TextOverflow.ellipsis to text, replacing Column with ListView for scrollable content, or using the Wrap widget for items that should flow onto multiple lines.
Q6: When should you use ListView instead of Column?
ListView whenever the content may grow taller than the available screen height. Column does not scroll and will overflow if its children exceed its bounds. ListView.builder is also more memory-efficient for long or dynamic lists because it only builds the widgets currently visible on screen, unlike Column which builds all children at once.
Quick-Copy Reference Snippet
Bookmark this — it combines alignment, flex sizing, and responsive width in one clean template:
// The complete layout toolkit in one snippet
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Takes 1/3 of available width
Expanded(
flex: 1,
child: Container(
height: 80,
color: Colors.red,
child: const Center(child: Text('1x')),
),
),
const SizedBox(width: 8), // fixed gap — not affected by flex
// Takes 2/3 of available width
Expanded(
flex: 2,
child: Container(
height: 80,
color: Colors.blue,
child: const Center(
child: Text('2x', style: TextStyle(color: Colors.white)),
),
),
),
],
)
See Layouts in a Real App
Every concept in this article is used throughout our notes app series — Column for the editor screen, Expanded to stretch the content field, Row inside the AppBar actions. See it all working in context:
| Article | Layout concepts in use |
|---|---|
| Notes App Part 1: Project Setup & Core UI | Column for editor layout, Expanded to fill content field height, Scaffold + AppBar |
| Notes App Part 2: Local Persistence & Polish | Column in ListTile subtitles, Card + Row composition patterns |
| Flutter Widgets Explained: Stateless vs Stateful | Column with mainAxisAlignment.center in every widget example |

Pingback: How to Add Custom Fonts in Flutter (Google Fonts, Local Fonts, and Best Practices)