Solving 'Vertical viewport was given unbounded height' in Nested ListViews
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: trueandphysics: NeverScrollableScrollPhysics()on the nested ListView, or wrap it inExpandedif 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:
- ListView is a scrollable widget that expands infinitely in its scroll direction
- Column provides unbounded height - it wants to be as tall as its children
- 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
- Check parent constraints: Use
LayoutBuilderto see available space - Add debug borders: Temporarily add
ContainerwithBorder.all()to visualize bounds - Read the error direction: "Vertical viewport" = height issue, "Horizontal viewport" = width issue
- 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
Expandedto give bounded constraints - Multiple lists: Combine
shrinkWrapwithSingleChildScrollView - 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
- Learn about Sliver widgets for advanced scrolling
- Explore ListView performance optimization
- Practice with Flutter layout constraints
Updated for Flutter 3.24+ and Dart 3.5+