Flutter: Flexible Spaces with Spacer Widget
Last updated:
8 min read

Spacer opens a flexible, invisible gap between widgets inside a Row or Column. Unlike a fixed SizedBox, the gap grows as the screen gets wider and shrinks as it gets narrower. This post focuses on Spacer alone: what is inside it, how it sizes gaps with flex, when mainAxisAlignment or SizedBox is the better tool, and why it throws inside scrollable areas. If you are deciding between Spacer, Expanded and Flexible, see the Expanded vs Flexible vs Spacer comparison; for the details of the flex math, see the Expanded post.
What Is Spacer?
Spacer's source is a single line: its build method returns Expanded(flex: flex, child: const SizedBox.shrink()). In other words, a Spacer is an Expanded wrapped around a zero-size box. Three things follow from that:
- It must be a direct child of a Row, Column or Flex, because Expanded must be.
- It fills its whole share; the share is whatever space the fixed-size siblings leave over.
- It is invisible and has no
childparameter; you cannot give it a color, a border or a tap area.
Basic Usage
Spacer in a Row
Row(
children: [
Text('Left'),
Spacer(),
Text('Right'),
],
)The Spacer fills all the empty space between "Left" and "Right", so the two texts sit at opposite ends.
Spacer in a Column
Column(
children: [
Text('Top'),
Spacer(),
Text('Bottom'),
],
)All the vertical space left between "Top" and "Bottom" becomes a gap. For this to work, the Column's height must be bounded; inside a Scaffold body it is fine, inside a scrollable area it throws (see below).
Pushing a single item to the end
A Spacer does not have to sit between two widgets. Put it first and it pushes the item after it to the end of the row:
Row(
children: [
Spacer(),
Text('On the right'),
],
)This code is valid and gives the same result as mainAxisAlignment: MainAxisAlignment.end. The advantage of Spacer is that the layout stays correct when you later add an item on the left.
Multiple Spacers
You can distribute gaps evenly by using more than one Spacer:
Row(
children: [
Text('Left'),
Spacer(),
Text('Center'),
Spacer(),
Text('Right'),
],
)Both Spacers have the same flex (the default, 1), so they split the free space in half. "Left" and "Right" hug the edges, "Center" sits between the two gaps. "Center" may not look exactly centered: the gaps are equal, but if "Left" and "Right" have different widths the middle text shifts slightly.
The flex Parameter
Spacer's flex parameter sets the ratio between gaps; it works exactly like the flex documented on the official Expanded widget API page:
Row(
children: [
Text('Left'),
Spacer(flex: 1), // 1 share
Text('Center'),
Spacer(flex: 2), // 2 shares
Text('Right'),
],
)The second Spacer takes twice as much space as the first: the gap between "Center" and "Right" is twice the gap between "Left" and "Center". flex must be a positive integer; writing Spacer(flex: 0) fails an assertion in debug mode.
Flex Ratios
Column(
children: [
Container(height: 50, color: Colors.red),
Spacer(flex: 1),
Container(height: 50, color: Colors.blue),
Spacer(flex: 3),
Container(height: 50, color: Colors.green),
],
)First the three boxes' 150 pixels of total height are subtracted, then the remaining space is split into 1+3 = 4 shares: the first gap takes 25% of the remaining space, the second 75%. The ratios are always computed on the remaining space, never on the total height.
Practical Examples
Example 1: An App Bar Style Layout
For a real top bar there is of course the AppBar widget; the point here is to see Spacer push the icons to the right while the title stays on the left:
Row(
children: [
IconButton(icon: Icon(Icons.menu), onPressed: () {}),
Text('Title', style: TextStyle(fontSize: 20)),
Spacer(),
IconButton(icon: Icon(Icons.search), onPressed: () {}),
IconButton(icon: Icon(Icons.more_vert), onPressed: () {}),
],
)Example 2: Card Footer
Row(
children: [
Text('Price: \$9.99'),
Spacer(),
ElevatedButton(
onPressed: () {},
child: Text('Buy'),
),
],
)Example 3: Pushing a Button to the Bottom
Column(
children: [
Padding(
padding: EdgeInsets.all(16),
child: Text('Top content'),
),
Spacer(),
Padding(
padding: EdgeInsets.all(16),
child: ElevatedButton(
onPressed: () {},
child: Text('Bottom button'),
),
),
],
)If this Column is a Scaffold body, the button sticks to the bottom of the screen. If the content can grow taller than the screen, this structure is not enough; the scrollable version is further down.
Example 4: Three-Part Layout
Row(
children: [
Icon(Icons.arrow_back),
Spacer(),
Text('Title'),
Spacer(),
Icon(Icons.settings),
],
)The two icons have the same width, so the title lands exactly in the middle. Add a second icon on the right and the title shifts left; when the items on the edges have different widths, a Stack or the centerTitle property of AppBar is a more reliable way to center.
Spacer vs SizedBox
SizedBox: Fixed Space
Row(
children: [
Text('Left'),
SizedBox(width: 20), // Always 20 pixels
Text('Right'),
],
)- Fixed-size gap
- Stays the same when the screen size changes
- Also works inside scrollable areas
- Details: sizing and spacing with SizedBox
Spacer: Flexible Space
Row(
children: [
Text('Left'),
Spacer(), // Fills all remaining space
Text('Right'),
],
)- Grows or shrinks with the remaining space
- Only works inside a bounded Row or Column
- Useful in responsive layouts
Rule of thumb: when the design specifies a distance between items (8, 12, 16 pixels), use SizedBox; when you need a "whatever is left" gap, use Spacer.
Spacer vs Expanded
Since a Spacer is an empty Expanded, the two snippets below give the same result:
Using Spacer
Row(
children: [
Text('Left'),
Spacer(),
Text('Right'),
],
)The Expanded Equivalent
Row(
children: [
Text('Left'),
Expanded(child: SizedBox.shrink()),
Text('Right'),
],
)Difference: Spacer is more readable and states the intent. Use Spacer for space only and Expanded for content.
Spacer and Expanded Together
Row(
children: [
Text('Fixed'),
Spacer(flex: 1),
Expanded(
flex: 2,
child: Container(
color: Colors.blue,
child: Text('Expanding'),
),
),
Spacer(flex: 1),
Text('Fixed'),
],
)After the widths of the two "Fixed" texts are subtracted, the remaining space is split into 1+2+1 = 4 shares: the blue area takes half, and each gap takes a quarter. Because Spacer and Expanded take part in the same distribution, gaps and content scale together.
Compared with mainAxisAlignment
In simple cases you can use mainAxisAlignment instead of a Spacer:
mainAxisAlignment.spaceBetween
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Left'),
Text('Right'),
],
)The Spacer Equivalent
Row(
children: [
Text('Left'),
Spacer(),
Text('Right'),
],
)Difference: spaceBetween makes every gap equal; Spacer puts the gap exactly where you want it and lets you weight it with flex. Do not combine the two in one row: according to the official docs, a Spacer takes up all the free space, so spaceBetween, spaceAround and spaceEvenly have no visible effect because there is nothing left to distribute. How a Row picks its own width and how that relates to alignment is covered in the mainAxisSize post.
Where Does Spacer Throw?
1. As a direct child of a ListView
// ❌ WRONG: throws
ListView(
children: [
Text('Top'),
Spacer(),
Text('Bottom'),
],
)Symptom: an error starting with Incorrect use of ParentDataWidget. Because a Spacer carries an Expanded inside, it can only be a direct child of a Row, Column or Flex; a ListView lays out its children as a scrollable list, not as a Flex. For gaps between list items use SizedBox(height: 16) or ListView.separated.
2. In a Column inside a SingleChildScrollView
// ❌ WRONG: throws
SingleChildScrollView(
child: Column(
children: [
Text('Top'),
Spacer(),
Text('Bottom'),
],
),
)Symptom: RenderFlex children have non-zero flex but incoming height constraints are unbounded. Here the Spacer is inside a Column, so it is in the right place; the problem is that the scroll area gives the Column infinite height, and "remaining space" cannot be computed from infinity. If a fixed gap is enough, use a SizedBox. Why scrollable structures offer unbounded height is explained in the ScrollView and list widgets post:
// ✅ CORRECT
SingleChildScrollView(
child: Column(
children: [
Text('Top'),
SizedBox(height: 100), // Fixed gap
Text('Bottom'),
],
),
)3. Inside a bounded area it is fine
// ✅ CORRECT
SizedBox(
height: 300, // Bounded height
child: Column(
children: [
Text('Top'),
Spacer(),
Text('Bottom'),
],
),
)As soon as the Column's height is known, the Spacer works again.
Pinning a Button to the Bottom of a Scrollable Page
A common request on sign-up and login forms: when the content is short, the button should sit at the bottom of the screen; when the keyboard opens or the content grows, the page should scroll. The solution in the SingleChildScrollView docs is to give the Column an "at least as tall as the viewport" rule:
LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight - 32),
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text('Welcome', style: TextStyle(fontSize: 24)),
const SizedBox(height: 12),
const Text('Tap the button below to continue.'),
const Spacer(),
FilledButton(onPressed: () {}, child: const Text('Continue')),
],
),
),
),
);
},
)LayoutBuilder provides the viewport height, ConstrainedBox makes the Column at least that tall (32 is the top and bottom padding combined), and IntrinsicHeight gives the Column an exact height, creating a "remaining space" the Spacer can compute. If the content grows, the Column grows with it and scrolling still works. The docs point out that IntrinsicHeight is expensive because it lays out the content twice, so use this structure for short forms, not long lists. For fixed-height items there is a cheaper route: drop IntrinsicHeight and the Spacer and give the Column mainAxisAlignment: MainAxisAlignment.spaceBetween.
When to Use It
Use Spacer:
- When you want a gap between items that grows and shrinks with the screen
- When gaps should be shared in different proportions (
flex) - When you want to push a group to the end of a row or column
- When working inside a bounded Row or Column
Don't use Spacer:
- As a direct child of a ListView or GridView
- In a Column inside a
SingleChildScrollView(unless you use the pattern above) - In a row that should wrap its content with
mainAxisSize: MainAxisSize.min, because a Spacer pulls the row to full width - When a fixed gap is enough (use
SizedBox)
Summary
Spacer:
- Is an empty
Expanded:Expanded(flex: flex, child: const SizedBox.shrink()) - Creates a flexible gap in a Row or Column by taking a share of the remaining space
- Weights gaps with the
flexparameter - Can also push a single item to the end on its own
- Throws inside scrollable areas that offer unbounded height and inside non-Flex parents
- Is meant for space; use Expanded for content
Frequently Asked Questions
Is there a performance difference between Spacer and Expanded(child: SizedBox())?
No. Spacer contains exactly that structure internally; the only difference is readability. Prefer Spacer to make your "this is just space" intent explicit in code.
Why does spaceBetween do nothing when there is a Spacer?
The Spacer takes all the free space in the row as its share. mainAxisAlignment only distributes the space left over after the children are laid out, and with a Spacer present that leftover is zero. Pick one: spaceBetween alone for equal gaps, Spacer alone for a gap in a specific place or with a ratio.
Can I give a Spacer a color or padding?
No, Spacer has no child parameter and is invisible. If you want a colored or decorated flexible area, use Expanded(child: Container(color: ...)).
Related Posts
Flutter: Expanding Space with Expanded Widget
A deep dive into Expanded: how the flex ratio is computed, nesting, why it only works inside Row and Column, and the three errors you see most often.
Flutter: Space and Alignment Control with mainAxisSize
max vs min in mainAxisSize, how constraints interfere, why mainAxisAlignment seems to do nothing, and recipes for dialogs, bottom sheets and button groups.
Flutter: Expanded vs Flexible vs Spacer, With Recipes
The difference between Expanded, Flexible and Spacer, loose vs tight fit, and four recipes that combine them: toolbar, card footer, form rows, split screen.