Loading devShakib…
Why auto-sizing text throws this in a Table cell or IntrinsicHeight — a framework limit rather than a bug — and how to size text without LayoutBuilder.
You put auto-sizing text in a Table cell, hot-reloaded, and got this:
LayoutBuilder does not support returning intrinsic dimensions.
Calculating the intrinsic dimensions would require running the layout
callback speculatively, which might mutate the live render object tree.
at _RenderLayoutBuilder.computeMaxIntrinsicHeight
The stack trace is long, it points at framework code you did not write, and the widget worked perfectly ten minutes ago in a Column. So the obvious first thought is that something is misconfigured.
Nothing is misconfigured. This is Flutter refusing to do something it genuinely cannot do, and understanding why tells you exactly which layouts will break and which will not — which is more useful than a workaround, because the same wall is waiting behind three or four different widgets.
Most Flutter layout is a single downward pass. A parent hands its child a set of constraints — you may be between 0 and 300 logical pixels wide — the child picks a size within them, and the parent positions it. One pass, no backtracking, which is why Flutter's layout is linear in the number of widgets rather than quadratic.
Some widgets cannot work that way. IntrinsicHeight has to make all its children the height of the tallest one, so it has to know how tall each wants to be before it can decide what to hand them. Table sizes a column to fit its widest cell by default. Baseline alignment needs to know where text sits before it can align on it.
For those, Flutter has a second protocol. A parent can ask a child a hypothetical question:
If I gave you 200 pixels of width, how tall would you want to be?
That question is computeMaxIntrinsicHeight, and the crucial part is the word hypothetical. Nothing is being laid out. The child is being asked to predict a size for constraints it has not been given and may never be given. The answer must be computed without touching the render tree, because the tree is in the middle of a layout pass and nothing may move yet.
LayoutBuilder exists to run your code with the constraints as an argument:
LayoutBuilder(
builder: (context, constraints) {
final size = _bestFontSizeFor(constraints.maxWidth);
return Text(label, style: TextStyle(fontSize: size));
},
)
This is how essentially every auto-sizing text package works, and it is a perfectly good idea. It measures the text at a candidate size, checks whether it fits the constraints it was handed, and adjusts.
Now look at what answering an intrinsic query would require. To tell the parent how tall it would be at 200 pixels wide, LayoutBuilder would have to run your builder callback with maxWidth: 200. Your callback constructs widgets. Those widgets get mounted into the element tree. That is a mutation — during a speculative query, for a layout that may never happen, possibly several times with different widths as the parent searches for a fit.
Flutter's own comment on the matter is blunt about it: doing so "might mutate the live render object tree". So it refuses, loudly, rather than corrupting your layout quietly. The error is the framework protecting you.
This is why it is not going to be fixed. It is not an oversight, it is the boundary between two layout protocols that cannot both be satisfied by the same widget.
There is a deeper reason this protocol exists at all, and knowing it makes the constraint feel less arbitrary.
Web layout is famously able to do this sort of thing — a CSS table sizes its columns to content without anyone thinking about it. It manages that by doing multiple passes over the tree, and by having a layout engine that can revisit decisions.
Flutter deliberately does not. Its layout is single-pass and linear: constraints go down, sizes come back up, each render object is visited once. That is a large part of why Flutter can hit 60 or 120fps on a deep tree — there is no risk of a pathological case where a small change triggers repeated re-layout of a large subtree.
The intrinsic protocol is the pressure valve. It lets a parent ask a question without triggering a real layout pass, at the cost of the query being a pure computation with no side effects. Widgets that use it are explicitly documented as more expensive, and IntrinsicHeight's own API docs warn that it can be O(N²) in the depth of the tree.
So when Flutter refuses to let LayoutBuilder participate, it is protecting the guarantee that makes the whole layout system fast. Allowing it would mean speculative builder invocations, which is a second pass through user code — the exact thing the architecture is designed to avoid.
The failure only appears when something above your widget asks an intrinsic question. That makes it feel random — the same widget works in one screen and throws in another. It is not random at all. These are the layouts that ask:
| Widget | Why it asks |
|---|---|
IntrinsicHeight | Matches children to the tallest one |
IntrinsicWidth | Matches children to the widest one |
Table | Columns size to their widest cell by default |
Baseline-aligned Row | Needs the text baseline before aligning |
ListView with shrinkWrap in some nestings | Sizes to content |
Table is the one that catches people, because nothing about writing Table(children: [...]) suggests you have just opted into a second layout protocol. You get a table. You put a heading in a cell. It explodes.
You do not need to memorise the table. There is a quicker heuristic: if a widget needs to know its children's sizes before deciding what constraints to give them, it will use intrinsics.
Column does not — it hands down its own constraints and takes whatever comes back. IntrinsicHeight does, by definition. Table does, because a column width is a function of every cell in it.
A second signal is the error itself. If a stack trace mentions computeMinIntrinsicWidth, computeMaxIntrinsicWidth, computeMinIntrinsicHeight or computeMaxIntrinsicHeight, you are in the intrinsic protocol regardless of which widget triggered it.
The way out is to stop asking the widget layer to measure things and drop to the layer that is allowed to. A RenderBox can measure text with a TextPainter whenever it likes, because a TextPainter is not part of the render tree — it is a standalone object that lays out a paragraph and reports its size. No mutation, no speculative element mounting.
That means the fitting can happen in two places that both work:
performLayout, for the real passcomputeMaxIntrinsicHeight, for the hypothetical question — because measuring with a TextPainter is exactly the kind of pure computation the intrinsic protocol requiresThe binary search for the largest font size that fits is the same algorithm the LayoutBuilder approach uses. The difference is entirely about where it runs.
This is what I built fit_text to do:
FitText('A headline that must never wrap', maxLines: 1, minFontSize: 12)
It works inside Table, IntrinsicHeight, IntrinsicWidth, baseline rows, Expanded, and unbounded constraints. The package's test suite contains a case that puts a LayoutBuilder-based sizer and a FitText in the same widget tree and asserts that the first throws while the second lays out — so the difference is pinned by a test rather than claimed in a README.
It has no dependencies, and it runs on all six platforms.
The distinction is worth stating precisely, because it is the whole trick.
TextPainter is a standalone object. Constructing one and calling layout() on it does not touch the render tree, does not mount elements, does not schedule a frame, and has no effect on anything outside the object itself. It is a pure function from (text, style, constraints) to a size, with some caching inside.
LayoutBuilder is the opposite. Its entire purpose is to run a callback that builds widgets, and building widgets means creating elements and mounting them into the tree. That is a side effect, and side effects are exactly what an intrinsic query is forbidden to have.
So the rule generalises beyond text. If you can compute your intrinsic size with a pure function, you can implement the intrinsic protocol. If computing it requires building widgets, you cannot, and no amount of cleverness at the widget layer will change that. Drop to a render object and compute directly.
You do not have to. The approach is the point, and it is reproducible:
RenderBox rather than composing widgets.TextPainter as a field. Call layout() on it with candidate styles.didExceedMaxLines and the resulting size against the constraints.computeMinIntrinsicWidth, computeMaxIntrinsicWidth, computeMinIntrinsicHeight and computeMaxIntrinsicHeight using the same measurement, and do not touch the tree in any of them.Point five is the one people miss. Table may ask each cell more than once while resolving column widths, and a binary search over twenty font sizes per query adds up quickly in a list.
If you do write your own, these are the ones that cost an evening each:
didExceedMaxLines is not enough on its own. Text can fit within maxLines and still be wider than the constraint if a single word cannot break. Check the painted size against the constraints as well as the line count, or a long unbroken token will silently overflow at your minimum size.textScaler must be part of your cache key. A user changing their system font size will not invalidate a cache keyed only on the string and the box.TextDirection is required and easy to forget, since it usually comes from the ambient Directionality. In a render object you have to thread it through yourself.minFontSize does not fit, decide deliberately whether to overflow, clip or ellipsise — and make it a parameter, because different call sites genuinely want different answers.LayoutBuilder cannot answer intrinsic queries because answering them means running your builder, and running your builder means mutating the tree during a speculative measurement. Any auto-sizing built on LayoutBuilder inherits that limit. Move the measurement into a RenderBox with a TextPainter and the limitation disappears, because measuring a paragraph was never the thing that needed the tree.
If you want it already done: fit_text on pub.dev, MIT licensed, no dependencies.