Website • Quickstart • Documentation • Playground
Inline Markdown-like formatting for Flutter — as drop-in replacements for Text and TextEditingController. Zero dependencies. Bold, italic, code, URL Link, highlights, super²/subscript₂.
⚠️ Upgrading from 1.1.x? Version 1.2.0 introduces strict flanking rules for formatting markers. Markers with surrounding whitespace — such as* spaced *— no longer trigger formatting. Update these to*not-spaced*. See Flanking Rules for details.
Replace Text with Textf and your strings render with bold, italic, code, highlights, links, and more.
Textf('Hello **Flutter**. Build for ==any screen==!');Replace TextEditingController with TextfEditingController to render formatting live in TextField as the user types — no extra widgets needed.
final controller = TextfEditingController();
TextField(controller: controller);1. Add the dependency:
flutter pub add textf2. Import the package:
import 'package:textf/textf.dart';3. Use it — that's it:
import 'package:flutter/material.dart';
import 'package:textf/textf.dart';
class MyWidget extends StatelessWidget {
final _controller = TextfEditingController();
@override
Widget build(BuildContext context) {
return Column(
children: [
// Drop-in for Text
Textf(
'**Bold**, *italic*, `code`, and [links](https://flutter.dev)',
style: TextStyle(fontSize: 16),
),
// Drop-in for TextEditingController
TextField(controller: _controller),
],
);
}
}Both widgets share the same formatting syntax and can be configured together with TextfOptions.
| Limitation | Reason |
|---|---|
| No block elements | Textf is for inline formatting only — no headings, lists, quotes, or images |
| Max 2 nesting levels | **bold _italic_** works, deeper nesting renders as plain text |
| Selection across links | Links use WidgetSpan, so selection can't span across them (Flutter limitation) |
| Widget placeholders | {key} placeholders render as literal text in TextfEditingController |
Textf is intentionally limited to inline formatting only. It is not a Markdown renderer.
✅ Great for:
- Chat messages and comment sections
- UI labels, captions, and tooltips
- Internationalized strings with inline emphasis
- User-generated content with simple formatting
- Performance-critical lists with many text widgets
❌ Not designed for:
- Full Markdown documents with headings, lists, or tables
- HTML rendering
- Block-level structure of any kind
Both Textf and TextfEditingController use the same syntax:
| Format | Syntax | Alternate | Result |
|---|---|---|---|
| Bold | **bold** |
__bold__ |
bold |
| Italic | *italic* |
_italic_ |
italic |
| Bold + Italic | ***bold italic*** |
___bold italic___ |
both |
| Strikethrough | ~~strike~~ |
||
| Underline | ++underline++ |
underline | |
| Highlight | ==highlight== |
highlight | |
| Inline code | `code` |
code |
|
| Superscript | ^super^ |
E = mc² | |
| Subscript | ~sub~ |
H₂O | |
| Link | [label](url) |
Flutter | |
| Placeholder | {key} |
(inserted widget) |
Formatting markers follow CommonMark-style flanking rules. Openers must not be followed by whitespace, and closers must not be preceded by whitespace:
*italic* ✅ * italic * ❌
**bold** ✅ ** bold ** ❌
This prevents accidental formatting of bullet points (* Item) and math expressions (2 * 3).
Up to 2 levels of nesting are supported. A third level renders as plain text — it never crashes or corrupts the surrounding output.
Textf('**Bold with _italic_ inside.**') // ✅ two levels — works
Textf('**_`three levels`_**') // ⚠️ third level renders as literal `three levels`Textf is forgiving. If a marker has no matching closer, it renders as plain text — it never crashes, and the rest of the string continues to format normally.
Textf('**unclosed and *italic*')
// renders: **unclosed and italic (italic still applies correctly)Use a backslash to render any marker literally. You can escape formatting markers as well as placeholders:
Textf(r'\**not bold\** and \{not_a_placeholder}')
// renders: **not bold** and {not_a_placeholder}A drop-in replacement for Flutter's Text widget. All Text parameters are supported identically — style, textAlign, maxLines, overflow, textScaler, locale, textDirection, strutStyle, semanticsLabel, and more.
Textf(
'**Bold**, *italic*, ~~strike~~, ++underline++, ==highlight==, '
'`code`, ^super^, ~sub~, [link](https://flutter.dev)',
style: TextStyle(fontSize: 16),
textAlign: TextAlign.center,
maxLines: 3,
overflow: TextOverflow.ellipsis,
)The .textf() extension lets you write formatting inline wherever you'd naturally write a string — useful in widget trees, i18n, and ARB-based localization:
// Directly in a widget tree
'**Status:** All systems operational'.textf()
// With style parameters
'Hello, **$username**!'.textf(style: TextStyle(fontSize: 18))
// From a localized string
AppLocalizations.of(context).welcomeMessage.textf()All Textf constructor parameters are available on .textf().
To extract clean, plain text from a formatted string (e.g., for search, analytics, or Semantics labels), use .stripFormatting():
'**Hello** [Flutter](.)!'.stripFormatting() // Returns: "Hello Flutter!"Embed arbitrary Flutter widgets inline using {key} syntax:
Textf(
'Made with {heart} using {flutter}',
placeholders: {
'heart': WidgetSpan(child: Icon(Icons.favorite, color: Colors.red)),
'flutter': WidgetSpan(child: FlutterLogo(size: 16)),
},
)Keys must be alphanumeric or underscores. Placeholders are not substituted in TextfEditingController — they render as literal {key} text there.
Links are rendered as tappable WidgetSpan elements. Handle taps by wrapping with TextfOptions (see TextfOptions for full configuration):
TextfOptions(
onLinkTap: (url, displayText) {
// Open in browser, push a route, or handle internally
debugPrint('Tapped: $url');
},
child: Textf('Visit [Flutter](https://flutter.dev)'),
)Note: Because links are
WidgetSpanelements, text selection cannot span across them. This is a Flutter platform limitation, not a Textf bug.
SelectionArea(
child: Textf('Select **this** formatted text!'),
)Textf caches parsed span trees using an LRU cache. Re-renders skip re-parsing when text, style, theme, and TextfOptions are unchanged — important for animated lists or chat feeds with many items. The cache invalidates automatically on changes.
To free memory in low-memory situations:
Textf.clearCache();A drop-in replacement for TextEditingController. Attach it to any TextField or TextFormField to render live formatting as the user types. The underlying text is always plain — the controller adds visual styling on top without affecting the stored value. Supports full IME (Input Method Editor) composing for seamless text entry in all languages.
Before building with this controller, be aware of the following constraints:
- Widget placeholders (
{key}) render as literal text — no widget substitution in editable fields - Links display the full
[text](url)syntax while editing — styled, but not tappable - Cross-line markers never pair across newlines — a marker on line 1 cannot accidentally format content on line 2
final controller = TextfEditingController();
TextField(controller: controller)With initial content:
TextfEditingController(text: 'Hello **bold**')MarkerVisibility controls how formatting markers appear while the user edits.
MarkerVisibility.always (default) — markers are always visible with dimmed styling. Predictable cursor behavior, works well on all platforms.
MarkerVisibility.whenActive — markers hide instantly when the cursor leaves the formatted span, giving a cleaner live-preview effect. During non-collapsed selection (e.g. drag-select on mobile), all markers hide automatically to prevent layout jumps that would shift selection handles.
TextfEditingController(markerVisibility: MarkerVisibility.whenActive)Change the mode at runtime and the field re-renders immediately:
controller.markerVisibility = MarkerVisibility.always;When text exceeds maxLiveFormattingLength characters, formatting is automatically disabled and the field renders as plain text. This prevents UI freezes on very long inputs.
TextfEditingController(maxLiveFormattingLength: 2500) // default: 5000Wrap the TextField with TextfOptions to control how formatted spans appear:
TextfOptions(
boldStyle: TextStyle(fontWeight: FontWeight.w900, color: Colors.deepOrange),
codeStyle: TextStyle(fontFamily: 'monospace', color: Colors.pink),
child: TextField(
controller: TextfEditingController(),
decoration: InputDecoration(labelText: 'Formatted input'),
),
)TextfOptions is an InheritedWidget that configures all descendant Textf widgets and TextfEditingController instances. Place it once near the top of a screen — or at app level — to apply consistent formatting throughout.
TextfOptions(
boldStyle: TextStyle(fontWeight: FontWeight.w900, color: Colors.deepOrange),
codeStyle: TextStyle(fontFamily: 'monospace', color: Colors.pink),
onLinkTap: (url, _) => debugPrint('Link tapped: $url'),
child: YourWidget(),
)| Property | Applies to |
|---|---|
boldStyle |
**bold** / __bold__ |
italicStyle |
*italic* / _italic_ |
boldItalicStyle |
***bold italic*** |
strikethroughStyle |
~~strike~~ |
underlineStyle |
++underline++ |
highlightStyle |
==highlight== |
codeStyle |
`code` |
superscriptStyle |
^super^ |
subscriptStyle |
~sub~ |
linkStyle |
Links — normal state |
linkHoverStyle |
Links — hover state |
| Property | Type / Description |
|---|---|
onLinkTap |
(String url, String displayText) → void |
onLinkHover |
(String url, String displayText, {required bool isHovering}) → void |
linkMouseCursor |
MouseCursor — shown over links (default: SystemMouseCursors.click) |
linkAlignment |
PlaceholderAlignment — vertical alignment of link spans (default: baseline) |
| Property | Description | Default |
|---|---|---|
scriptFontSizeFactor |
Font size multiplier for super/subscripts | 0.6 |
superscriptBaselineFactor |
Vertical offset factor for superscripts | -0.4 |
subscriptBaselineFactor |
Vertical offset factor for subscripts | 0.2 |
TextfOptions uses two different strategies depending on the property type.
Style properties merge down the tree. A parent's color and a child's font weight both apply — neither is discarded. This mirrors how TextStyle.merge works across DefaultTextStyle in Flutter, and means you can define broad styles at a high level and refine them locally without losing the parent context.
TextfOptions(
boldStyle: TextStyle(color: Colors.red), // parent: red color
child: TextfOptions(
boldStyle: TextStyle(fontWeight: FontWeight.w900), // child: heavy weight
child: Textf('**Red AND heavy**'), // both apply ✅
),
)Callback and cursor properties use nearest-ancestor-wins. The closest TextfOptions in the tree takes effect. This prevents double-firing when options are nested — only one handler should respond to a tap.
TextfOptions(
onLinkTap: (url, _) => debugPrint('root handler'),
child: TextfOptions(
onLinkTap: (url, _) => debugPrint('inner handler'), // this one wins
child: Textf('[tap me](https://example.com)'),
),
)Textf automatically adapts to the active ThemeData — no configuration needed:
- Links use
colorScheme.primary - Code background uses
colorScheme.surfaceContainer - Code text uses
colorScheme.onSurfaceVariant
Override any theme default with TextfOptions:
TextfOptions(
linkStyle: TextStyle(color: Colors.teal, fontWeight: FontWeight.w600),
child: Textf('A [custom colored](https://example.com) link.'),
)- Text Scaling — Respects
MediaQuery.textScalerOf(context)and system font scaling settings - Screen Readers — Links are wrapped in
Semantics(link: true)for TalkBack and VoiceOver - RTL Support — Bidirectional text and RTL languages work correctly throughout
| Feature | Textf | Full Markdown Packages |
|---|---|---|
| Bundle size | Tiny | Large |
| Dependencies | Zero | Multiple |
| Parse complexity | O(N) | Often O(N²) or worse |
| API familiarity | Identical to Text |
Custom widgets |
| Live editing | ✅ | Rarely |
| Block elements | ❌ | ✅ |
| Best for | Inline formatting | Document rendering |
Full documentation on pub.dev.
Textf ships with an AI agent skill. Once you have textf as a dependency, run:
dart pub global activate skills
skills getThis installs Textf's skill into your project, giving AI coding agents (Claude Code, Cursor, Cline, and others) full knowledge of the API, formatting syntax, and best practices — enabling accurate, idiomatic suggestions without needing to read the docs.
Contributions are welcome! Please feel free to submit a Pull Request.
MIT License — see LICENSE for details.
About the name: Textf is inspired by C's
printf(print formatted).Textf(Text formatted) brings the same idea to Flutter — simple, efficient, and unsurprising.



