İçeriğe geç / Skip to content / Zum Inhalt

Flutter: Flexible Spaces with Spacer Widget

Ahmet Balaman

Last updated:

8 min read

FlutterSpacerLayoutFlexRowColumn
Flutter: Flexible Spaces with Spacer Widget

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 child parameter; 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: () {}),
  ],
)
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'),
  ],
)

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 flex parameter
  • 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: ...)).

Comments