The curious case of Intrinsic Width.
A multiple approach, step-wise problem and solution

A multiple approach, step-wise problem and solution
Free Link for Readers
So we want to create a List Widget that:
- Makes sure that all the items of the list are equal in width
- If the widget takes up more space than the screen's width, it should be able to scroll the items as needed.
In this article, we’re going to discuss the possible solutions through which we can develop this widget.
We are using starter code in this article but it can work without it nonetheless.
When we first build the app, it looks something like this:

Using a MeasureSize widget
The obvious solution that comes to mind, is to: As Flutter render each item, measure the longest item width and apply that width to all items.
Let’s start with a widget that helps measure the size of each list item.
// # equal_width_list
//
// Created by: Dhruvam
// Date: 16/03/25
// Time: 7:30 pm
import 'package:flutter/widgets.dart';
class MeasureSize extends StatefulWidget {
const MeasureSize({required this.child, required this.onChange, super.key});
final Widget child;
final void Function(Size size) onChange;
@override
MeasureSizeState createState() => MeasureSizeState();
}
class MeasureSizeState extends State<MeasureSize> {
final GlobalKey _key = GlobalKey();
@override
void initState() {
// Wait for the widget to build.
WidgetsBinding.instance.addPostFrameCallback((_) => _notifySize());
super.initState();
}
void _notifySize() {
// find the context of this widget in the widget tree
final context = _key.currentContext;
if (context == null) return;
// get the size of the widget after it has been built
final size = context.size;
// call the onChange function with the size
// notifies the parent widget of the size of this widget
if (size != null) widget.onChange(size);
}
@override
Widget build(BuildContext context) {
return Container(key: _key, child: widget.child);
}
}
What this code does:
- This widget will take the item of the list as a child and uses a global key to find the context of this widget to extract properties
- Inside
_notifySize
function, we find the context of this widget after the widget has been built using theaddPostFrameCallback
. - After we get the widget size, we pass it down the callback to notify the parent widget.
Now we can use this widget inside our EqualWidthItemList
or any widget of your choice.
// # equal_width_list
//
// Created by: Dhruvam
// Date: 16/03/25
// Time: 8:06 pm
import 'package:equal_width_list/utils/measure_size.dart';
import 'package:flutter/material.dart';
class EqualWidthItemList extends StatefulWidget {
const EqualWidthItemList({
required this.itemCount,
required this.itemBuilder,
super.key,
});
final int itemCount;
final Widget Function(BuildContext context, int index) itemBuilder;
@override
State<EqualWidthItemList> createState() => _EqualWidthItemListState();
}
class _EqualWidthItemListState extends State<EqualWidthItemList> {
double _maxWidth = 0;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(widget.itemCount, (index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: MeasureSize(
onChange: (size) {
if (size.width > _maxWidth) {
setState(() {
_maxWidth = size.width;
});
}
},
child: SizedBox(
width: _maxWidth == 0 ? null : _maxWidth,
child: widget.itemBuilder(context, index),
),
),
);
}),
),
);
}
}
So this list does a few things in this code:
- Each time a Row child is built and rendered, the size calculation is done and we call
setState
to update each item's size. - As all the children are laid out, the list is rendered repeatedly to update each item’s size.
Code available at Checkpoint 1

Cons of This Approach
Double Layout Pass (Inefficient)
- The MeasureSize widget first renders each item to determine its width.
- Then, the list is rebuilt to apply the largest width to all items.
- Performance issue: This causes extra layout calculations, especially with a large number of items.
Delayed UI Updates
- Initially, the items may have varying widths.
- Once _maxWidth is determined, the UI rebuilds with equal widths.
- User sees a flicker as items adjust dynamically.
Stateful Overhead
- _maxWidth is stored in the widget state, causing unnecessary rebuilds when resizing occurs.
- If the item count is large, this can slow down UI performance.
Doesn’t Handle Dynamic Content Changes Well
- If items change dynamically (e.g., new items added, text changes), _maxWidth might not be recalculated properly unless the widget is reset.
So there’s Intrinsic Width too!
So the Flutter Documentation says:
A widget that sizes its child to the child’s maximum intrinsic width.
So let’s understand by breaking this sentence down:
What is Intrinsic Width?
Intrinsic width is the minimum width required for a widget to display its content without clipping or wrapping unnecessarily. For example:
- A Text widget has an intrinsic width equal to the width of the longest unbreakable word or full sentence.
- A Button with text has an intrinsic width equal to the space needed to fit the text properly.
What Does “Maximum Intrinsic Width” Mean?
“Maximum intrinsic width” means the widest that the child naturally wants to be. And the parent forces the child to take this width.
When we see this code:
Row(
children: [
Container(color: Colors.blue, child: Text("Short")),
Container(color: Colors.red, child: Text("Very very long text")),
],
)
The widths of the containers will be different because text naturally takes only the space it needs.
But if we use intrinsic width:
IntrinsicWidth(
child:
children: [
Expanded(child: Container(color: Colors.blue, child: Text("Short"))),
Expanded(child: Container(color: Colors.red, child: Text("Very very long text"))),
],
),
)
Now both containers will have the same width! Because IntrinsicWidth ensures that both children take the same maximum intrinsic width (in this case, the width of the longest text).
But you will notice we need the expanded here to keep the containers of the same width. If we remove the expanded, the items will take only the required space.
This is a sharp and important question! Let’s understand the definition again.
“A widget that sizes its child to the child’s maximum intrinsic width.”
This means that IntrinsicWidth forces its child (which is the entire Row in this case) to take the width of its widest child.

So if we don’t use Intrinsic width widget, the code looks like this:
@override
Widget build(BuildContext context) {
final texts = ['Hi', 'Flutter', 'Yo'];
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(
texts.length,
(index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(texts[index]),
);
},
),
),
);
}
The Row’s width is simply the sum of its children’s widths. So the Row width = 50 + 120 + 40= 210px.
And if we apply the Intrinsic width widget, the Row’s width is again 210px. This is because the Row is forced to be at least as wide as its widest child (120px). BUT, this does not mean it becomes 120 * 3 = 360px. Instead, the Row just ensures its total width is at least 120px. If the natural sum of children's width is already more than 120px, then IntrinsicWidth does nothing!
Now when you add the expanded widget, something interesting happens:
- IntrinsicWidth looks at all children and finds the widest one (120px in this case). It then ensures the entire Row is at least that wide. But individual children are still their original sizes at this point.
- Expanded forces each child to take equal width inside the Row. Since IntrinsicWidth already decided the total Row width, this becomes the width that Expanded has to work with. Since the row does not have an unbounded width anymore, Expanded can divide the space equally.
- Each child gets the width of the widest child (120px). Expanded distributes the total Row width equally among children. Since IntrinsicWidth made sure the Row is at least 120px per child, this becomes the new width for all children.
So if we apply this knowledge to our code, EqualWidthItemList
widget now looks something like this:
// # equal_width_list
//
// Created by: dhruvam
// Date: 16/03/25
// Time: 8:06 pm
import 'package:equal_width_list/utils/measure_size.dart';
import 'package:flutter/material.dart';
class EqualWidthItemList extends StatelessWidget {
const EqualWidthItemList({
required this.itemCount,
required this.itemBuilder,
super.key,
});
final int itemCount;
final Widget Function(BuildContext context, int index) itemBuilder;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: IntrinsicWidth(
child: Row(
children: List.generate(
itemCount,
(index) {
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: itemBuilder(context, index),
),
);
},
),
),
),
);
}
}
Code available at Checkpoint 2
But, Flutter documentation also says this:
This class is relatively expensive, because it adds a speculative layout pass before the final layout phase. Avoid using it where possible. In the worst case, this widget can result in a layout that is O(N²) in the depth of the tree.
Let’s break this down too:
What is a Layout Pass?
A layout pass in Flutter is the process where Flutter calculates how big each widget should be and where it should be placed on the screen. This happens in two main phases:
- The “Measure” Phase (Constraints Phase)
- Parent tells the child how much space it can use (its constraints).
- Example: A parent might say, “You can be between 0 and 300 pixels wide.”
2. The “Layout” Phase (Size & Position Phase)
- The child decides its size based on the constraints and tells the parent.
- The parent then positions the child in the UI.
What happens when you use IntrinsicWidth?
IntrinsicWidth adds an extra layout pass:
1. First Pass: It asks each child, “What’s your natural (intrinsic) width?”
2. Second Pass: It sets the row’s width to match the widest child.
3. Third Pass: It lays out each child again inside that new width.
Since this involves multiple passes, it can slow down performance, especially in lists or deep widget trees.
Verdict? So which is a better approach?

Use IntrinsicWidth for small, static lists if you want a quick and simple solution. Use MeasureSize for dynamic lists or better performance, even for 10–20 items.
There’s another approach that we can use, a CustomMultiChildLayout, but let’s keep it for another day.
Hope you liked the information presented so far. Questions and doubts are welcome in the comments.