I didn't find a direct way to do this.
But here is a possible solution using Hover on the whole RichText
and then identifying which TextSpan
is the target, and whether or not it has a tooltip.

Though, it's not an easy ride. Buckle up!
I tried to keep my application as simple as possible:
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
title: 'TextSpan Hover Demo',
home: HomePage(),
),
);
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final _textKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
alignment: Alignment.center,
padding: EdgeInsets.all(16.0),
child: RichTextTooltipDetector(
textKey: _textKey,
child: RichText(
key: _textKey,
text: TextSpan(
text: 'Hello ',
children: <TextSpan>[
TextSpan(
text: 'bold',
style: TextStyle(fontWeight: FontWeight.bold),
semanticsLabel: 'Tooltip: Yeah! It works',
),
TextSpan(text: ' world!'),
],
),
),
),
),
);
}
}
This RichTextTooltipDetector
is where I handle the Tooltips as OverlayEntries
.
My RichTextTooltipDetector
is just a MouseRegion
on which I will handle the hover events. This event gives me the local position of the mouse. This position, together with the RichText GlobalKey, is all we need to identify whether we have a tooltip to show or not:
- From the
RichText
Global Key, I get the RenderParagraph
- From this
RenderParagraph
and the localPosition
of the PointerHoverEvent
, I get the `InlineSpan, if any
- When I defined the
TextSpan
, I highjacked the semanticsLabel
. This let me know easily if I need to display a Tooltip or not.
The rest is just basic OverlayEntry management:
- Creating an
OverlayEntry
based on the BuildContext
, an offset
, and the Tooltip text
- Displaying the OverlayEntry with
Overlay.of(context).insert(_tooltipOverlay);
- After 1 second, hiding the
OverlayEntry
with _tooltipOverlay?.remove();
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class _RichTextTooltipDetectorState extends State<RichTextTooltipDetector> {
OverlayEntry _tooltipOverlay;
Timer _timer;
RenderParagraph get _renderParagraph =>
widget.textKey.currentContext?.findRenderObject() as RenderParagraph;
InlineSpan _span(PointerHoverEvent event, RenderParagraph paragraph) {
final textPosition = paragraph.getPositionForOffset(event.localPosition);
return paragraph.text.getSpanForPosition(textPosition);
}
String _tooltipText(PointerHoverEvent event, RenderParagraph paragraph) {
final span = _span(event, paragraph);
return span is TextSpan &&
span.semanticsLabel != null &&
span.semanticsLabel.startsWith('Tooltip: ')
? span.semanticsLabel.split('Tooltip: ')[1]
: '';
}
OverlayEntry _createTooltip(
BuildContext context, String text, Offset offset) {
return OverlayEntry(
builder: (context) => Positioned(
left: offset.dx,
top: offset.dy,
child: Material(
elevation: 4.0,
child: Container(
decoration: BoxDecoration(
color: Colors.white70,
border: Border.all(color: Colors.black87, width: 3.0),
),
padding: EdgeInsets.all(8.0),
child: Text(text),
),
),
),
);
}
void _showTooltip() {
Overlay.of(context).insert(_tooltipOverlay);
_timer = Timer(Duration(seconds: 1), () => _hideTooltip());
}
void _hideTooltip() {
_timer?.cancel();
_tooltipOverlay?.remove();
_tooltipOverlay = null;
_timer = null;
}
void _handleHover(BuildContext context, PointerHoverEvent event) {
_hideTooltip();
final paragraph = _renderParagraph;
if (event is! PointerHoverEvent || paragraph == null) return;
final tooltipText = _tooltipText(event, paragraph);
if (tooltipText.isNotEmpty) {
RenderBox renderBox = context.findRenderObject();
var offset = renderBox.localToGlobal(event.localPosition);
_tooltipOverlay = _createTooltip(context, tooltipText, offset);
_showTooltip();
}
}
@override
Widget build(BuildContext context) {
return MouseRegion(
onHover: (event) => _handleHover(context, event),
child: widget.child,
);
}
}