← Back to Blog

Solving 'Vertical viewport was given unbounded height' in Nested ListViews

January 15, 20269 Minutes Read
flutterdartuilistviewwidgetstroubleshootingmobile developmentapp developmentflutter errorsnested widgets

Solving "Vertical viewport was given unbounded height" in Nested ListViews

You're building a Flutter app, and you need a ListView inside a Column. You write the code, run it, and boom:

Vertical viewport was given unbounded height.
Viewports expand in the scrolling direction to fill their container.

This error is one of the most frustrating Flutter layout errors because it's not immediately obvious why it happens or how to fix it. The error occurs when a scrollable widget (like ListView, GridView, or SingleChildScrollView) is placed inside a parent that doesn't provide bounded height constraints.

Quick Solution: Use shrinkWrap: true and physics: NeverScrollableScrollPhysics() on the nested ListView, or wrap it in Expanded if it should take remaining space. The key is understanding that ListView needs bounded constraints in its scroll direction.

In this guide, we'll explain why this error happens, when to use shrinkWrap: true vs Expanded, and provide copy-paste-ready solutions for common scenarios.


Why This Error Happens

To understand the fix, you need to understand Flutter's constraint system:

  1. ListView is a scrollable widget that expands infinitely in its scroll direction
  2. Column provides unbounded height - it wants to be as tall as its children
  3. Conflict: ListView says "I need infinite height" while Column says "I'll be as tall as you need"

This creates a circular dependency that Flutter cannot resolve, hence the error.

Key Concept: Scrollable widgets need bounded constraints in their scroll direction. When placed in a Column or Row, they don't get those bounds.


Understanding shrinkWrap vs Expanded

Before diving into solutions, let's clarify the two main approaches:

shrinkWrap: true

  • What it does: Makes the ListView only as tall as its content (instead of infinite)
  • When to use: When you want the ListView to size itself based on its items
  • Performance: Can be slower with many items (renders all at once)
  • Scrolling: Usually combined with physics: NeverScrollableScrollPhysics() to disable scrolling

Expanded

  • What it does: Forces the ListView to take all available space from its parent
  • When to use: When you want the ListView to fill remaining space in a Column
  • Performance: Better for long lists (lazy loading, viewport-based rendering)
  • Scrolling: ListView scrolls normally within the expanded space

Scenario 1: ListView Inside a Column (Most Common)

The Problem

Column(
  children: [
    Text('Header'),
    ListView(
      children: [
        ListTile(title: Text('Item 1')),
        ListTile(title: Text('Item 2')),
        ListTile(title: Text('Item 3')),
      ],
    ),
    Text('Footer'),
  ],
)

Error: Vertical viewport was given unbounded height

Solution 1: Use shrinkWrap (For Short Lists)

If your list is short and you want it to size itself:

Column(
  children: [
    Text('Header'),
    ListView(
      shrinkWrap: true,
      physics: NeverScrollableScrollPhysics(), // Disable scrolling
      children: [
        ListTile(title: Text('Item 1')),
        ListTile(title: Text('Item 2')),
        ListTile(title: Text('Item 3')),
      ],
    ),
    Text('Footer'),
  ],
)

Why this works: shrinkWrap: true makes the ListView calculate its height based on content, removing the need for infinite height.

When to use:

  • Short lists (fewer than 20-30 items)
  • You want the list to be exactly as tall as its content
  • The parent Column should scroll if needed

Solution 2: Use Expanded (For Long Lists)

If your list can be long and should take remaining space:

Column(
  children: [
    Text('Header'),
    Expanded(
      child: ListView(
        children: [
          ListTile(title: Text('Item 1')),
          ListTile(title: Text('Item 2')),
          ListTile(title: Text('Item 3')),
          // Can have many items - will scroll
        ],
      ),
    ),
    Text('Footer'),
  ],
)

Why this works: Expanded gives the ListView bounded height constraints (the remaining space in the Column).

When to use:

  • Long lists (many items)
  • You want lazy loading and viewport rendering
  • The ListView should scroll within a fixed space

Solution 3: Make the Entire Column Scrollable

If you want everything to scroll together:

SingleChildScrollView(
  child: Column(
    children: [
      Text('Header'),
      ...List.generate(
        10,
        (index) => ListTile(title: Text('Item ${index + 1}')),
      ),
      Text('Footer'),
    ],
  ),
)

Note: This doesn't use ListView at all - just a Column with spread items. Use this when you want a single scrollable area.


Scenario 2: ListView.builder Inside Column

The Problem

Column(
  children: [
    AppBar(title: Text('My App')),
    ListView.builder(
      itemCount: 100,
      itemBuilder: (context, index) {
        return ListTile(title: Text('Item $index'));
      },
    ),
  ],
)

Solution: Use Expanded

ListView.builder is designed for long lists, so Expanded is usually the right choice:

Column(
  children: [
    AppBar(title: Text('My App')),
    Expanded(
      child: ListView.builder(
        itemCount: 100,
        itemBuilder: (context, index) {
          return ListTile(title: Text('Item $index'));
        },
      ),
    ),
  ],
)

Why Expanded: ListView.builder uses lazy loading - it only builds visible items. Expanded gives it bounded constraints while maintaining performance.


Scenario 3: Multiple ListViews in a Column

The Problem

Column(
  children: [
    Text('Section 1'),
    ListView(children: [/* items */]),
    Text('Section 2'),
    ListView(children: [/* items */]),
    Text('Section 3'),
    ListView(children: [/* items */]),
  ],
)

Solution: Use shrinkWrap for All

When you have multiple lists, use shrinkWrap for each:

SingleChildScrollView(
  child: Column(
    children: [
      Text('Section 1'),
      ListView(
        shrinkWrap: true,
        physics: NeverScrollableScrollPhysics(),
        children: [/* items */],
      ),
      Text('Section 2'),
      ListView(
        shrinkWrap: true,
        physics: NeverScrollableScrollPhysics(),
        children: [/* items */],
      ),
      Text('Section 3'),
      ListView(
        shrinkWrap: true,
        physics: NeverScrollableScrollPhysics(),
        children: [/* items */],
      ),
    ],
  ),
)

Why: Each ListView sizes itself, and the parent SingleChildScrollView handles overall scrolling.


Scenario 4: ListView Inside a Row (Horizontal)

The Problem

Row(
  children: [
    Text('Categories:'),
    ListView(
      scrollDirection: Axis.horizontal,
      children: [
        Chip(label: Text('Tech')),
        Chip(label: Text('Design')),
        Chip(label: Text('Business')),
      ],
    ),
  ],
)

Error: Horizontal viewport was given unbounded width

Solution: Use shrinkWrap or Expanded

Row(
  children: [
    Text('Categories:'),
    Expanded(
      child: ListView(
        scrollDirection: Axis.horizontal,
        children: [
          Chip(label: Text('Tech')),
          Chip(label: Text('Design')),
          Chip(label: Text('Business')),
        ],
      ),
    ),
  ],
)

Or with shrinkWrap:

Row(
  children: [
    Text('Categories:'),
    Flexible(
      child: ListView(
        shrinkWrap: true,
        scrollDirection: Axis.horizontal,
        physics: NeverScrollableScrollPhysics(),
        children: [
          Chip(label: Text('Tech')),
          Chip(label: Text('Design')),
          Chip(label: Text('Business')),
        ],
      ),
    ),
  ],
)

Scenario 5: Nested ListView in a CustomScrollView

The Problem

You want a SliverAppBar with a ListView below it:

CustomScrollView(
  slivers: [
    SliverAppBar(title: Text('My App')),
    SliverList(
      delegate: SliverChildListDelegate([
        ListView(children: [/* items */]), // Error!
      ]),
    ),
  ],
)

Solution: Use SliverList Directly

Don't nest ListView - use SliverList or SliverFixedExtentList:

CustomScrollView(
  slivers: [
    SliverAppBar(title: Text('My App')),
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) {
          return ListTile(title: Text('Item $index'));
        },
        childCount: 100,
      ),
    ),
  ],
)

Why: CustomScrollView uses slivers, not regular widgets. Use sliver widgets inside it.


Comparison Table: shrinkWrap vs Expanded

Feature shrinkWrap: true Expanded
Height Based on content Takes all available space
Performance Renders all items Lazy loads (viewport-based)
Scrolling Usually disabled Enabled
Use Case Short lists, exact sizing Long lists, fill space
Constraints Unbounded → Bounded Bounded → Bounded
Best For Static content, few items Dynamic content, many items

Decision Tree: Which Solution to Use?

Is your ListView inside a Column/Row?
│
├─ Yes → Does it have many items (>20)?
│   │
│   ├─ Yes → Use Expanded
│   │
│   └─ No → Use shrinkWrap: true + NeverScrollableScrollPhysics()
│
└─ No → Is it in a CustomScrollView?
    │
    └─ Yes → Use SliverList instead

Common Mistakes

1. Forgetting physics: NeverScrollableScrollPhysics()

// ❌ Wrong - ListView will still try to scroll
ListView(
  shrinkWrap: true,
  children: [/* items */],
)

// ✅ Correct
ListView(
  shrinkWrap: true,
  physics: NeverScrollableScrollPhysics(),
  children: [/* items */],
)

2. Using Expanded with unbounded parent

// ❌ Wrong - Column has unbounded height
Column(
  children: [
    Expanded(child: ListView(...)), // Still won't work!
  ],
)

// ✅ Correct - Wrap Column in SizedBox or Container with height
SizedBox(
  height: MediaQuery.of(context).size.height,
  child: Column(
    children: [
      Expanded(child: ListView(...)),
    ],
  ),
)

3. Using shrinkWrap with ListView.builder for long lists

// ❌ Wrong - Defeats the purpose of ListView.builder
ListView.builder(
  shrinkWrap: true, // Renders all items at once!
  itemCount: 1000,
  itemBuilder: (context, index) => ListTile(...),
)

// ✅ Correct - Use Expanded
Expanded(
  child: ListView.builder(
    itemCount: 1000,
    itemBuilder: (context, index) => ListTile(...),
  ),
)

Performance Considerations

shrinkWrap Performance

  • Renders all items immediately - no lazy loading
  • Use only for short lists (< 30 items typically)
  • Can cause jank with many items

Expanded Performance

  • Lazy loading - only renders visible items
  • Better for long lists (hundreds or thousands of items)
  • Smooth scrolling with proper itemExtent

Optimizing ListView.builder

Expanded(
  child: ListView.builder(
    itemCount: items.length,
    itemExtent: 56.0, // Fixed height improves performance
    cacheExtent: 250.0, // Pre-render items outside viewport
    itemBuilder: (context, index) {
      return ListTile(title: Text('Item $index'));
    },
  ),
)

Advanced: Using LayoutBuilder for Dynamic Solutions

Sometimes you need to choose the solution based on available space:

LayoutBuilder(
  builder: (context, constraints) {
    // If we have enough space, use shrinkWrap
    // Otherwise, use Expanded with scrolling
    if (constraints.maxHeight > 600) {
      return Column(
        children: [
          Text('Header'),
          ListView(
            shrinkWrap: true,
            physics: NeverScrollableScrollPhysics(),
            children: shortList,
          ),
        ],
      );
    } else {
      return Column(
        children: [
          Text('Header'),
          Expanded(
            child: ListView(children: shortList),
          ),
        ],
      );
    }
  },
)

Debugging Tips

  1. Check parent constraints: Use LayoutBuilder to see available space
  2. Add debug borders: Temporarily add Container with Border.all() to visualize bounds
  3. Read the error direction: "Vertical viewport" = height issue, "Horizontal viewport" = width issue
  4. Use Flutter Inspector: Visualize widget tree and constraints in real-time

Real-World Example: E-commerce Product List

Here's a complete example combining multiple techniques:

class ProductListPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Products')),
      body: Column(
        children: [
          // Filter chips - horizontal scrollable
          Container(
            height: 50,
            child: ListView(
              scrollDirection: Axis.horizontal,
              children: [
                Chip(label: Text('All')),
                Chip(label: Text('Electronics')),
                Chip(label: Text('Clothing')),
                // More chips...
              ],
            ),
          ),
          Divider(),
          // Product list - vertical scrollable with Expanded
          Expanded(
            child: ListView.builder(
              itemCount: products.length,
              itemBuilder: (context, index) {
                return ProductCard(product: products[index]);
              },
            ),
          ),
        ],
      ),
    );
  }
}

Conclusion

The "Vertical viewport was given unbounded height" error happens because scrollable widgets need bounded constraints. The solution depends on your use case:

  • Short lists: Use shrinkWrap: true + NeverScrollableScrollPhysics()
  • Long lists: Use Expanded to give bounded constraints
  • Multiple lists: Combine shrinkWrap with SingleChildScrollView
  • CustomScrollView: Use sliver widgets instead of nested ListView

Remember: shrinkWrap = "size yourself", Expanded = "take available space". Choose based on your content and performance needs.


Next Steps

Updated for Flutter 3.24+ and Dart 3.5+