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.1.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 stateRow(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