Flutter RichText and TextSpan: Multi-Style Text in One Widget

Coding Liquids blog cover featuring Sagnik Bhattacharya for the Flutter RichText and TextSpan guide, with a TextSpan tree, inline WidgetSpan icon, and tappable span visuals.
Coding Liquids blog cover featuring Sagnik Bhattacharya for the Flutter RichText and TextSpan guide.

Sooner or later you need one word bold in the middle of a sentence, a coloured price next to plain label text, or a tappable "Terms" link inside a paragraph. A plain Text can't do that — it styles the whole string at once. RichText and its building block TextSpan let you compose many styles into a single, flowing piece of text. This guide covers the span tree, the Text.rich shortcut, inline widgets, tappable spans, and accessibility, with paste-ready code.

The Complete Flutter Guide course thumbnail

The Complete Flutter Guide: Build Android, iOS and Web apps

Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.

Enrol now

Every snippet below is paste-ready against current stable Flutter. If you haven't styled a single string yet, the Text and TextStyle guide is the prerequisite — every span here carries a TextStyle, so the properties there apply directly.

Follow me on Instagram@sagnikteaches

We'll start with the TextSpan tree, see why RichText needs an explicit base style, switch to the friendlier Text.rich, embed icons with WidgetSpan, make a span tappable, and finish on accessibility.

Connect on LinkedInSagnik Bhattacharya

If watching helps more than reading, the channel builds inline links and mixed-style labels into real screens so you can see the span tree take shape.

Subscribe on YouTube@codingliquids

The TextSpan tree

A TextSpan has its own text and style, plus a list of children spans. Children inherit the parent's style and override only what they change, so you set a base once and layer differences on top.

RichText(
  text: TextSpan(
    style: const TextStyle(color: Colors.black87, fontSize: 16),
    children: [
      const TextSpan(text: 'Total: '),
      TextSpan(
        text: '£49',
        style: const TextStyle(
          fontWeight: FontWeight.bold,
          color: Colors.green,
        ),
      ),
      const TextSpan(text: ' per seat'),
    ],
  ),
)

The whole thing flows and wraps as one paragraph — break a line mid-phrase and the bold price stays bold. That's the key advantage over lining up separate Text widgets in a Row, which can't wrap as a single sentence.

RichText needs a base style

One gotcha catches everyone: RichText does not read DefaultTextStyle. A bare Text inherits a sensible colour and size from its Material ancestors, but RichText starts from nothing, so if you omit the root span's style your text can render in the unstyled default. Always set a base style on the root TextSpan — often by pulling in the ambient default explicitly.

RichText(
  text: TextSpan(
    style: DefaultTextStyle.of(context).style, // adopt the ambient style
    children: const [
      TextSpan(text: 'Reads like the rest of the screen, '),
      TextSpan(text: 'with emphasis here.',
          style: TextStyle(fontStyle: FontStyle.italic)),
    ],
  ),
)

Text.rich: the friendlier form

Most of the time you don't need raw RichText at all. Text.rich takes the same TextSpan tree but behaves like a normal Text — it inherits DefaultTextStyle, so the base-style gotcha disappears, and it accepts familiar arguments like maxLines and textAlign.

Text.rich(
  TextSpan(
    children: [
      const TextSpan(text: 'By continuing you agree to our '),
      TextSpan(
        text: 'Terms',
        style: TextStyle(
          color: Colors.indigo,
          fontWeight: FontWeight.w600,
        ),
      ),
      const TextSpan(text: '.'),
    ],
  ),
  textAlign: TextAlign.center,
)

Reach for Text.rich by default and drop to RichText only when you specifically don't want default-style inheritance. They share the same span model, so nothing else changes.

The Complete Flutter Guide course thumbnail

Build real UI text, not lorem ipsum

The Complete Flutter Guide covers inline links, badges, and rich labels inside production screens from first principles.

Enrol now

Inline icons with WidgetSpan

A TextSpan only holds text, but a WidgetSpan can hold any widget and lay it out inline with the surrounding glyphs — an icon, a small avatar, a badge. It's how you put a verified tick right after a username without breaking the line.

Text.rich(
  TextSpan(
    children: [
      const TextSpan(text: 'sagnikteaches '),
      WidgetSpan(
        alignment: PlaceholderAlignment.middle,
        child: Icon(Icons.verified, size: 16, color: Colors.blue),
      ),
    ],
  ),
)

The alignment argument controls how the widget sits against the text baseline — PlaceholderAlignment.middle centres a small icon on the line, which looks right for inline ticks and chips.

Tappable spans

To make one span respond to taps, attach a TapGestureRecognizer with an onTap callback. Because a recogniser holds resources, create it in a StatefulWidget and dispose of it — leaking recognisers is a real, if quiet, memory bug.

// Inside a State class:
late final TapGestureRecognizer _termsTap;

@override
void initState() {
  super.initState();
  _termsTap = TapGestureRecognizer()..onTap = _openTerms;
}

@override
void dispose() {
  _termsTap.dispose(); // don't leak the recogniser
  super.dispose();
}

// In build():
TextSpan(
  text: 'Terms',
  style: const TextStyle(color: Colors.indigo),
  recognizer: _termsTap,
)

This is the canonical pattern for inline links in legal copy, "read more" toggles, and @-mentions. For a whole-widget tap rather than a span, a GestureDetector or button is simpler — recognisers are specifically for sub-spans of text.

Accessibility

Screen readers announce the concatenated text of the span tree, which is usually correct. When the visual text differs from what should be read — say a price written "£49" that you'd like read as "forty-nine pounds" — pass a semanticsLabel on the Text.rich to override the spoken version while keeping the visual one intact.

Common mistakes

  • Forgetting RichText's base style. It doesn't inherit DefaultTextStyle; set the root span's style or use Text.rich.
  • Leaking a TapGestureRecognizer. Create it in initState and call dispose() on it.
  • Using a Row of Texts for one sentence. It can't wrap mid-phrase; use a span tree so the styled pieces flow together.
  • Reaching for RichText to style a whole string. If every character shares a style, a plain Text is simpler.
  • Misaligning WidgetSpan children. Set PlaceholderAlignment so inline icons sit on the baseline, not above or below the line.

Frequently asked questions

What is the difference between RichText and Text in Flutter?

Text styles one string uniformly; RichText renders a tree of TextSpan pieces, each with its own style. RichText doesn't inherit DefaultTextStyle, so set a base style or use Text.rich.

How do I make part of a Text tappable in Flutter?

Attach a TapGestureRecognizer to the TextSpan and set onTap. Create it in a StatefulWidget and dispose of it.

How do I put an icon inline with text in Flutter?

Use a WidgetSpan in the span tree; it can hold any widget and lays it out inline, aligned via PlaceholderAlignment.

Should I use RichText or multiple Text widgets in a Row?

Use RichText/Text.rich for one flowing sentence that wraps together; a Row of Texts can't wrap mid-phrase and breaks awkwardly on narrow screens.

Further reads

Keep going with the tutorials that pair with this guide:

Sources: Flutter documentation — RichText, TextSpan, Text.rich, WidgetSpan, PlaceholderAlignment, and TapGestureRecognizer API references, plus the Typography guide (docs.flutter.dev). Verified against current stable Flutter.