Mobile Development 11 min read

Implementing Gradient Text in Flutter Using ShaderMask, TextStyle.foreground, CustomPainter, and ExtendedText

This article explores multiple techniques for creating gradient text in Flutter—including ShaderMask, TextStyle.foreground, a custom CustomPainter, and the ExtendedText package's GradientConfig—detailing their code implementations, limitations, and how to handle WidgetSpan, emoji, and selective gradient rendering.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Implementing Gradient Text in Flutter Using ShaderMask, TextStyle.foreground, CustomPainter, and ExtendedText

Preface

One day a product manager said the app's text looked too plain and wanted a colorful, gradient effect, providing an example image. The author notes that achieving such an effect is not a trivial task.

Exploring Solutions

ShaderMask

ShaderMask(
  shaderCallback: (Rect bounds) {
    return const LinearGradient(
      colors:
[Colors.blue, Colors.red],
    ).createShader(bounds);
  },
  child: const Text(
    'I will be affected by the gradient effect',
    style: TextStyle(color: Colors.white),
  ),
);

ShaderMask applies a gradient to the whole widget, but it cannot treat WidgetSpan or emoji individually, especially for multi‑line text where the gradient is applied to the entire block.

Adding WidgetSpan and Emoji

ShaderMask(
  shaderCallback: (Rect bounds) {
    return const LinearGradient(
      colors:
[Colors.blue, Colors.red],
    ).createShader(bounds);
  },
  child: Text.rich(
    TextSpan(
      children:
[
        WidgetSpan(
          child: Container(
            width: 20,
            height: 20,
            color: Colors.red,
          ),
        ),
        const TextSpan(
          text: '🤭I will be affected by the gradient effect🤭',
          style: TextStyle(color: Colors.green),
        ),
        WidgetSpan(
          child: Container(
            width: 20,
            height: 20,
            color: Colors.blue,
          ),
        ),
      ],
    ),
    style: const TextStyle(color: Colors.white),
  ),
);

Even with WidgetSpan and emoji, the gradient still covers the whole area, making it impossible to apply different gradients per line or per span.

TextStyle.foreground

Text(
  'I will be affected by the gradient effect',
  style: TextStyle(
    foreground: Paint()
      ..shader = const LinearGradient(
        colors:
[Colors.blue, Colors.red],
      ).createShader(
        // Text's relative position rectangle
        const Rect.fromLTWH(0, 0, 100, 50),
      ),
  ),
);

Using TextStyle.foreground with a shader gives more control, but you must manually calculate the rectangle that matches the rendered text, and it still cannot selectively exclude WidgetSpan or emoji.

CustomPainter Experiment

Because the built‑in APIs are limited, the author creates a CustomPainter to draw gradient text more flexibly.

class _GradientTextPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // Create TextPainter to layout the text
    final TextPainter textPainter = TextPainter(
      text: const TextSpan(text: 'Gradient Text'),
      textDirection: TextDirection.ltr,
    );
    textPainter.layout(maxWidth: size.width);

    // Define the rectangle that matches the text size
    final Size textSize = textPainter.size;
    final Rect shaderRect = Rect.fromLTWH(0, 0, textSize.width, textSize.height);

    // Create the gradient shader
    final Shader shader = const LinearGradient(
      colors:
[Colors.blue, Colors.red],
    ).createShader(shaderRect);

    // Paint the gradient using BlendMode.srcIn
    final Paint paint = Paint()
      ..shader = shader
      ..blendMode = BlendMode.srcIn;
    canvas.saveLayer(const Offset(0, 0) & size, Paint());
    textPainter.paint(canvas, const Offset(0, 0));
    canvas.drawRect(shaderRect, paint);
    canvas.restore();
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

By drawing the text first, then painting a gradient rectangle with BlendMode.srcIn , any shape can be used to achieve the desired effect.

ExtendedText Package

The extended_text package adds a GradientConfig parameter to simplify gradient text rendering.

GradientConfig _config = GradientConfig(
  gradient: const LinearGradient(
    colors:
[Colors.blue, Colors.red],
  ),
  ignoreRegex: GradientConfig.ignoreEmojiRegex,
  ignoreWidgetSpan: true,
  renderMode: GradientRenderMode.fullText,
  blendMode: BlendMode.srcIn,
  beforeDrawGradient: (PaintingContext context, TextPainter textPainter, Offset offset) {
    // custom logic before drawing gradient
  },
);

GradientConfig lets you specify the gradient, decide whether to ignore emojis (via ignoreRegex ) or WidgetSpan , choose the render mode (fullText, line, selection, word, character), and set the blend mode.

GradientRenderMode Enum

enum GradientRenderMode {
  fullText, // apply to the whole text
  line,     // apply per line
  selection,// apply to a TextSelection region
  word,     // apply per word
  character // apply per character
}

Different modes allow fine‑grained control over where the gradient is painted.

Handling WidgetSpan and Emoji

To prevent the gradient from affecting WidgetSpan or emoji, you can either draw the gradient before or after the widget spans based on the ignoreWidgetSpan flag.

// Draw gradient before widget spans
if (_gradientConfig != null && _gradientConfig!.ignoreWidgetSpan) {
  drawGradient(context, offset);
}
paintInlineChildren(context, offset);
// Draw gradient after widget spans
if (_gradientConfig != null && !_gradientConfig!.ignoreWidgetSpan) {
  drawGradient(context, offset);
}

Ignoring Specific Characters with Regex

Emoji or other characters can be excluded from the gradient by providing a regular expression and clipping those regions.

final List
boxes =
[];
if (_gradientConfig != null && _gradientConfig!.ignoreRegex != null) {
  _gradientConfig!.ignoreRegex!.allMatches(_textPainter.plainText).forEach((RegExpMatch match) {
    final int start = match.start;
    final int end = match.end;
    final TextSelection textSelection = TextSelection(baseOffset: start, extentOffset: end);
    boxes.addAll(_textPainter.getBoxesForSelection(textSelection));
  });
}

Custom Gradient Shapes via Callback

The beforeDrawGradient callback lets developers draw custom shapes (e.g., heart or star) and clip the canvas before applying the gradient.

void _beforeDrawGradient(PaintingContext context, TextPainter textPainter, Offset offset) {
  final Rect rect = offset & textPainter.size;
  Path? path;
  switch (_drawGradientShape) {
    case DrawGradientShape.heart:
      path = clipHeart(rect);
      break;
    case DrawGradientShape.star:
      path = clipStar(rect);
      break;
    case DrawGradientShape.none:
      break;
  }
  if (path != null) {
    context.canvas.drawPath(
      path,
      Paint()
        ..color = Colors.red
        ..style = PaintingStyle.stroke
        ..strokeWidth = 1,
    );
    context.canvas.clipPath(path);
  }
}

Conclusion

The gradient text capability is supported in Flutter 3.10.0 and later (package version ≥ 11.0.9). By customizing the text‑painting pipeline—whether through ShaderMask , TextStyle.foreground , a CustomPainter , or the ExtendedText package—developers can create rich, flexible gradient effects while handling special spans and emojis as needed.

FlutterMobile DevelopmentUIgradient-textcustom-painterextended_text
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.