Building Rich Text Components in Flutter with Textf (Part 2)
From help tooltips to product cards: a hands-on guide to text formatting, custom styling, and interactive links.
- Part 1: Textf 1.0: Inline Text Formatting for Flutter Without the Overhead
- Part 2: Building Rich Text Components in Flutter with Textf
- Part 3: Textf 2.0: Ready for Flutter’s Decoupled Future
In my previous article, I introduced Textf — a lightweight alternative to full Markdown packages for inline text formatting. If you haven’t read it, the short version: Textf is a drop-in replacement for Flutter’s Text widget that adds formatting with familiar syntax like bold and italic.
Now let’s build something real.
By the end of this tutorial, you’ll have created three production-ready components: a help tooltip, a product description card, and a styled notification. Along the way, you’ll learn everything Textf offers — from basic formatting to custom styling to theme integration.
Let’s start.
Setup
Add Textf to your pubspec.yaml:
dependencies:
textf: ^1.0.0Run flutter pub get, then import:
import 'package:textf/textf.dart';Step 1: Basic Formatting
The simplest use of Textf is direct replacement:
// Before
Text('Welcome to our app')
// After
Textf('Welcome to our **app**')Every formatting option uses intuitive syntax:
Textf(
'**Bold** and *italic* and ***both***. '
'~~Strikethrough~~ and ++underline++ and ==highlight==. '
'Even `inline code` works.',
)Escaping Special Characters
Need a literal asterisk? Use a backslash. Using a raw string (r’…’) makes this cleaner:
Textf(r'Ratings: \*\*\*\*\* (5 stars)')
// Renders: Ratings: ***** (5 stars)Step 2: Superscript and Subscript
Textf handles scientific notation cleanly:
Textf('Einstein\'s famous equation: E = mc^2^')
Textf('The chemical formula for water: H~2~O')Step 3: Interactive Links
Links in Textf aren’t just styled text — they’re tappable.
Basic Links
Textf('Visit [flutter.dev](https://flutter.dev) for documentation.')Handling Taps
Wrap your widget tree with TextfOptions to provide a tap handler. If you are using url_launcher, the pattern looks like this:
Get Philipp Gerber’s stories in your inbox
Join Medium for free to get updates from this writer.
(Note: Ensure you’ve configured your AndroidManifest/Info.plist for url_launcher appropriately)
import 'package:url_launcher/url_launcher.dart';
TextfOptions(
onLinkTap: (url, displayText) async {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
}
},
child: Textf('Check out [this guide](https://flutter.dev)!'),
)Hover Effects
For desktop and web, add hover feedback:
TextfOptions(
onLinkTap: (url, _) => launchUrl(Uri.parse(url)),
onLinkHover: (url, displayText, {required isHovering}) {
// Update cursor, show preview, log analytics...
},
child: Textf('Read [the documentation](https://flutter.dev).'),
)Step 4: Custom Styling with TextfOptions
Default styles work well, but real apps need brand customization. TextfOptions controls how every format type renders.
TextfOptions(
boldStyle: TextStyle(
fontWeight: FontWeight.w900,
color: Colors.indigo,
),
italicStyle: TextStyle(
fontStyle: FontStyle.italic,
color: Colors.purple,
),
highlightStyle: TextStyle(
backgroundColor: Colors.lightBlueAccent[200],
color: Colors.black87,
),
child: Textf('This is **bold** and *italic* and ==highlighted==.'),
)Link Styling
Links have specific options:
TextfOptions(
linkStyle: TextStyle(
color: Colors.teal,
decoration: TextDecoration.none,
),
linkHoverStyle: TextStyle(
color: Colors.teal[700],
decoration: TextDecoration.underline,
),
linkMouseCursor: SystemMouseCursors.click,
onLinkTap: (url, _) => launchUrl(Uri.parse(url)),
child: Textf('Visit [our website](https://flutter.dev).'),
)Step 5: Theme Integration
Textf reads your app’s ThemeData automatically.
- Links use colorScheme.primary.
- Code blocks use colorScheme.surfaceContainer background.
- Code text uses colorScheme.onSurfaceVariant.
MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.system,
home: Scaffold(
body: Textf('This adapts to **light** and *dark* mode.'),
),
)Practical Build: Help Tooltip
Let’s combine what we’ve learned into a real component.
class HelpTooltip extends StatelessWidget {
final String content;
const HelpTooltip({required this.content, super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[300]!),
),
child: TextfOptions(
// Handle links within the tooltip
onLinkTap: (url, _) => launchUrl(Uri.parse(url)),
child: Textf(
content,
style: TextStyle(fontSize: 14, color: Colors.grey[800]),
),
),
);
}
}
// Usage
HelpTooltip(
content: 'Press **Ctrl+S** to save. '
'Files are stored in `~/Documents`. '
'See [keyboard shortcuts](https://example.com/shortcuts) for more.',
)The formatting lives in the string, not the code. Easy to update, easy to localize.
Practical Build: Product Card
Now let’s build a component that showcases styling and theming.
class ProductCard extends StatelessWidget {
final String name;
final String description;
final String price;
final String? salePrice;
final String detailsUrl;
const ProductCard({
required this.name,
required this.description,
required this.price,
this.salePrice,
required this.detailsUrl,
super.key,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: TextfOptions(
boldStyle: TextStyle(
fontWeight: FontWeight.w700,
color: theme.colorScheme.primary,
),
highlightStyle: TextStyle(
backgroundColor: Colors.orange[100],
color: Colors.orange[900],
fontWeight: FontWeight.w600,
),
// Links look like standard text but clickable
linkStyle: TextStyle(
color: theme.colorScheme.secondary,
decoration: TextDecoration.none,
),
onLinkTap: (url, _) => launchUrl(Uri.parse(url)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Textf(
'**$name**',
style: theme.textTheme.titleLarge,
),
SizedBox(height: 8),
Textf(
description,
style: theme.textTheme.bodyMedium,
),
SizedBox(height: 12),
_buildPrice(),
SizedBox(height: 8),
Textf('[View details]($detailsUrl)'),
],
),
),
),
);
}
Widget _buildPrice() {
if (salePrice != null) {
return Textf(
'~~\$$price~~ ==\$$salePrice== — *Limited time!*',
style: TextStyle(fontSize: 18),
);
}
return Textf(
'**\$$price**',
style: TextStyle(fontSize: 18),
);
}
}Practical Build: Notification Banner
One more example showing different styling:
class NotificationBanner extends StatelessWidget {
final String message;
final VoidCallback onDismiss;
const NotificationBanner({
required this.message,
required this.onDismiss,
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
color: Colors.blue[50],
child: Row(
children: [
Icon(Icons.info_outline, color: Colors.blue[700]),
SizedBox(width: 12),
Expanded(
child: TextfOptions(
boldStyle: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.blue[900],
),
linkStyle: TextStyle(
color: Colors.blue[700],
fontWeight: FontWeight.w600,
),
onLinkTap: (url, _) => launchUrl(Uri.parse(url)),
child: Textf(
message,
style: TextStyle(color: Colors.blue[800]),
),
),
),
IconButton(
icon: Icon(Icons.close),
onPressed: onDismiss,
),
],
),
);
}
}Next Steps
You now have everything you need to build production-ready formatted text in Flutter. The patterns from this tutorial — help tooltips, product cards, notifications — transfer to any component that needs rich text.
Explore further:
- 📖 API Documentation: pub.dev/documentation/textf
- 💻 Source Code: github.com/PhilippHGerber/textf
Questions? Found a bug? Contributions welcome. Happy formatting.








