Loading devShakib…
Text that shrinks to fit its box, correctly. Works inside Expanded, Row, IntrinsicHeight and unbounded constraints, where LayoutBuilder-based auto-sizing gives up.

Text that shrinks to fit its box — including in the places auto-sizing usually breaks.
FitText('A headline that must never wrap', maxLines: 1, minFontSize: 12)
Flutter has no built-in way to make text fit: Text overflows or clips. The established workaround measures inside a LayoutBuilder, and that works right up until the widget lands somewhere Flutter needs an intrinsic size first:
LayoutBuilder does not support returning intrinsic dimensions.
at _RenderLayoutBuilder.computeMaxIntrinsicHeight
That is a hard framework limitation, not a bug someone forgot to fix. It rules LayoutBuilder-based auto-sizing out of:
IntrinsicHeight and IntrinsicWidthTable cells — columns size with intrinsics by defaultFitText does the fitting inside its own RenderBox, during layout. Intrinsics are answered honestly, so it works in all of those. There is a test in the suite that pins the LayoutBuilder failure alongside FitText succeeding in the same tree, so the difference is verified rather than asserted.
dependencies:
fit_text: ^0.2.0
// Shrink to fit one line, never below 12.
FitText('Long product name', maxLines: 1, minFontSize: 12);
// Grow to fill, but stay on your type scale.
FitText('42', presetFontSizes: [16, 24, 32, 48, 64]);
// Rich text — nested sizes scale proportionally.
FitText.rich(
TextSpan(text: 'Total ', children: [
TextSpan(text: '£42', style: TextStyle(fontWeight: FontWeight.bold)),
]),
maxLines: 1,
);
Both constructors are const, like Text.
Independently fitted labels land on different sizes, which looks wrong across a row of buttons. A group makes them agree on the smallest size any member needed, so they match and all of them still fit.
final labels = FitTextGroup(); // hold this in state
Row(children: [
Expanded(child: FitText('Save', group: labels, maxLines: 1)),
Expanded(child: FitText('Discard changes', group: labels, maxLines: 1)),
]);
Members find their own size first and agree on the next frame, so a group settles one frame after its contents change.
minFontSize | Floor. Below this the text overflows instead of shrinking further. |
maxFontSize | Ceiling. Defaults to the inherited style's size, so text shrinks but never grows unless you ask. |
stepGranularity | Increment between candidate sizes. |
presetFontSizes | An explicit scale, instead of a stepped range. |
group | Share a size with other members. |
Everything Text takes — textAlign, maxLines, softWrap, overflow, strutStyle, textScaler, locale, textWidthBasis — behaves the same way.
Finding the size is a bisection, not a scan: about log2((max - min) / step) text layouts, so six or so for the defaults. It re-runs only when the text, style, or constraints change, and RenderFitText.fittedFontSize exposes what it chose if you need to react to it.
The API is deliberately close, so most call sites change only the widget name.
| auto_size_text | fit_text |
|---|---|
AutoSizeText(...) | FitText(...) |
AutoSizeText.rich(...) | FitText.rich(...) |
AutoSizeGroup() | FitTextGroup() |
group: myGroup | group: myGroup |
| overflowReplacement: | overflowReplacement: |
| wrapWords: false | wrapWords: false |
Neither package supports text selection.
One difference worth knowing: overflowReplacement stays mounted whether or not it is used, collapsed to zero size — the same trade IndexedStack makes. That is what lets the swap happen inside a single layout pass with no frame of overflowing text, but it means an expensive replacement widget is built even when it never shows. Keep it cheap.
MIT © K M Shahriar Hossain