Flutter "Vertical viewport was given unbounded height": How to Fix

Coding Liquids tutorial cover featuring Sagnik Bhattacharya for fixing Flutter unbounded height errors.
Coding Liquids tutorial cover featuring Sagnik Bhattacharya for fixing Flutter unbounded height errors.

If you have spent more than a few days writing Flutter code, you have almost certainly encountered the dreaded red screen of death proclaiming: "Vertical viewport was given unbounded height." This layout exception typically strikes when you are trying to combine scrollable views, such as placing a ListView directly inside a Column. It is a rite of passage for Flutter developers, signalling a collision between what you want the UI to do and how the rendering engine actually calculates dimensions.

Follow me on Instagram@sagnikteaches

In this guide, we will break down exactly why this layout error occurs by exploring Flutter's underlying constraints model. Rather than just applying a quick hack, we will look at four distinct ways to fix the problem, ranging from simple wrapper widgets to restructuring your UI with Slivers. You will learn how to choose the right solution based on your app's architecture and performance requirements.

Connect on LinkedInSagnik Bhattacharya

Before diving into the code, keep the golden rule of Flutter layout in mind: Constraints go down, sizes go up, and the parent sets the position. Understanding this single principle will not only help you resolve unbounded height exceptions but will also make you significantly faster at debugging any visual behaviour in your applications.

Subscribe on YouTube@codingliquids
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

Understanding the Constraints Model

To understand why the "unbounded height" error happens, we must look at how Flutter negotiates widget sizes. When a parent widget needs to lay out a child, it passes down a set of constraints (minimum and maximum width, minimum and maximum height). The child must choose a size that fits within those boundaries.

A Column is a Flex widget. By default, it wants to take up as much vertical space as its parent allows, but it passes unbounded (infinite) vertical constraints down to its children. It essentially tells its children, "Take as much vertical space as you need."

A ListView, on the other hand, is a scrollable viewport. Its default behaviour is to expand to fill all the space given to it in its scroll direction. When a Column tells a ListView it can be infinitely tall, the ListView happily accepts and attempts to draw an infinitely tall list. Because rendering an infinitely large widget is impossible, Flutter throws an exception to stop the app from freezing.

The Bad Code That Causes the Crash

Here is the classic setup that triggers the exception. If you run this, your screen will turn red.

import 'package:flutter/material.dart';

class BrokenLayoutExample extends StatelessWidget {
  const BrokenLayoutExample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Unbounded Height Error')),
      body: Column(
        children: [
          const Padding(
            padding: EdgeInsets.all(16.0),
            child: Text('My Favourite Items', style: TextStyle(fontSize: 24)),
          ),
          // CRASH: This ListView is given infinite vertical space by the Column
          ListView.builder(
            itemCount: 50,
            itemBuilder: (context, index) => ListTile(
              title: Text('Item $index'),
            ),
          ),
        ],
      ),
    );
  }
}

Fix 1: The Expanded Widget (The Best Default)

The most common and performant way to fix a ListView inside a Column is to wrap the scrollable widget in an Expanded widget. Expanded is a special widget that only works inside Flex containers (like Column or Row).

When you wrap the ListView in an Expanded, you change the conversation between the parent and the child. Instead of passing down an infinite constraint, the Column first measures all of its non-flexible children (like your header text). It then takes whatever bounded vertical space is left over and forces the Expanded widget to fill exactly that remaining space. The ListView now has a hard limit on its height, and the error disappears.

import 'package:flutter/material.dart';

class ExpandedFixExample extends StatelessWidget {
  const ExpandedFixExample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Expanded Fix')),
      body: Column(
        children: [
          const Padding(
            padding: EdgeInsets.all(16.0),
            child: Text('My Favourite Items', style: TextStyle(fontSize: 24)),
          ),
          // FIX: Expanded forces the ListView to only take the remaining space
          Expanded(
            child: ListView.builder(
              itemCount: 50,
              itemBuilder: (context, index) => ListTile(
                title: Text('Item $index'),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

You should favour this approach whenever you want a header or footer to remain fixed on the screen while the list scrolls independently in the middle.

Fix 2: Using shrinkWrap (With Caution)

Sometimes, you do not want the ListView to take up all the remaining screen space. You might want the ListView to be exactly as tall as its children, and you want the entire page to scroll together (perhaps by placing the Column inside a SingleChildScrollView).

In this scenario, you can use the shrinkWrap property on the ListView. Setting shrinkWrap: true tells the ListView to evaluate the height of all its children and size itself to match them, rather than expanding infinitely. You will usually pair this with NeverScrollableScrollPhysics so the list does not scroll independently of the outer page.

import 'package:flutter/material.dart';

class ShrinkWrapFixExample extends StatelessWidget {
  const ShrinkWrapFixExample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ShrinkWrap Fix')),
      // The whole page scrolls
      body: SingleChildScrollView(
        child: Column(
          children: [
            const Padding(
              padding: EdgeInsets.all(16.0),
              child: Text('Page Header', style: TextStyle(fontSize: 24)),
            ),
            // FIX: shrinkWrap makes the ListView calculate its exact height
            ListView.builder(
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              itemCount: 10, // Keep this number small!
              itemBuilder: (context, index) => ListTile(
                title: Text('Item $index'),
              ),
            ),
            const Padding(
              padding: EdgeInsets.all(16.0),
              child: Text('Page Footer', style: TextStyle(fontSize: 24)),
            ),
          ],
        ),
      ),
    );
  }
}

Warning: Use shrinkWrap sparingly. Because it must calculate its total height, it forces Flutter to build and render every single item in the list at once, completely destroying the lazy-loading performance benefits of ListView.builder. If your list has hundreds of items, shrinkWrap will cause severe UI jank and memory bloat. Only use it for small, static lists (e.g., fewer than 20 items).

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

Fix 3: Bounding Height With a SizedBox

If you want a scrollable list that occupies a specific, hardcoded portion of the screen, you can constrain the ListView by wrapping it in a SizedBox with a defined height.

This is particularly useful for horizontal carousels embedded within vertical feeds, or when building a specific UI component like a fixed-height dropdown menu or a bottom sheet.

import 'package:flutter/material.dart';

class SizedBoxFixExample extends StatelessWidget {
  const SizedBoxFixExample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('SizedBox Fix')),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Padding(
            padding: EdgeInsets.all(16.0),
            child: Text('Horizontal Carousel', style: TextStyle(fontSize: 24)),
          ),
          // FIX: SizedBox provides a strict height constraint to the ListView
          SizedBox(
            height: 150, // Hardcoded bounded height
            child: ListView.builder(
              scrollDirection: Axis.horizontal,
              itemCount: 20,
              itemBuilder: (context, index) => Container(
                width: 150,
                margin: const EdgeInsets.all(8.0),
                color: Colors.blueAccent,
                child: Center(
                  child: Text(
                    'Card $index',
                    style: const TextStyle(color: Colors.white),
                  ),
                ),
              ),
            ),
          ),
          const Expanded(
            child: Center(child: Text('Other content below')),
          ),
        ],
      ),
    );
  }
}

Fix 4: The Production Approach Using Slivers

If you find yourself reaching for shrinkWrap: true because you want a scrolling header, a scrolling list of items, and a scrolling footer all moving together, you should stop using Column entirely. The architecturally correct way to build complex, unified scrolling layouts in Flutter is to use a CustomScrollView with Slivers.

Slivers are portions of a scrollable area. Unlike a Column, a CustomScrollView is designed to handle multiple scrollable elements efficiently. SliverList lazy-loads its children just like a normal ListView.builder, meaning you get perfect 60fps performance regardless of how many items you have, without ever encountering an unbounded height error.

import 'package:flutter/material.dart';

class SliverFixExample extends StatelessWidget {
  const SliverFixExample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Slivers Fix')),
      // FIX: Replace Column and ListView with a CustomScrollView
      body: CustomScrollView(
        slivers: [
          // Use SliverToBoxAdapter for standard widgets like headers
          const SliverToBoxAdapter(
            child: Padding(
              padding: EdgeInsets.all(16.0),
              child: Text('Scrolling Header', style: TextStyle(fontSize: 24)),
            ),
          ),
          // Use SliverList for the dynamic, lazy-loaded content
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (context, index) => ListTile(
                title: Text('Sliver Item $index'),
              ),
              childCount: 1000, // Highly performant, even with 1000 items
            ),
          ),
          // Another adapter for the footer
          const SliverToBoxAdapter(
            child: Padding(
              padding: EdgeInsets.all(16.0),
              child: Text('Scrolling Footer', style: TextStyle(fontSize: 24)),
            ),
          ),
        ],
      ),
    );
  }
}

While Slivers require slightly more boilerplate to set up, they represent the most robust solution for production applications. They elegantly bypass constraints issues by ensuring all elements share the same scrolling viewport.

Verifying Your Constraints With DevTools

When you are designing complex views and want to ensure you are not accidentally passing unbounded constraints before a crash happens, you should use the Flutter DevTools Widget Inspector. Specifically, the Layout Explorer feature allows you to visualise the exact constraints passed down the widget tree.

Run your app in debug mode, open DevTools, and select your Column or ListView. The Layout Explorer will show you the minimum and maximum dimensions allocated to that widget. If you see an infinity symbol () next to the height of a scrollable view, you know you need to intervene with an Expanded, a SizedBox, or a structural change to Slivers before that view attempts to render dynamic content.

Further reads

Keep going with these related tutorials from this site.