textf 2.0.0-dev.3
textf: ^2.0.0-dev.3 copied to clipboard
Inline Markdown-like formatting for Flutter - as drop-in replacements for `Text` and `TextEditingController`. Bold, italic, code, [URL links](.), highlights, super/subscript.
Textf #
Website • Quickstart • 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.
Two Drop-in Replacements #
Textf — Formatted display text #
Replace Text with Textf and your strings render with bold, italic, code, highlights, links, and more.
Textf('Hello **Flutter**. Build for ==any screen==!');

TextfEditingController — Live formatting in text fields #
Replace TextEditingController with TextfEditingController to render formatting live in TextField as the user types — no extra widgets needed.
final controller = TextfEditingController();
TextField(controller: controller);

Quick Start #
1. Add the dependency:
flutter pub add textf
2. 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.
Limitations #
| Limitation | Reason |
|---|---|
| Inline-first | ATX headings (#–######) are the one block-level exception — no lists, quotes, tables, 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 |
CommonMark Feature Support Status #
Textf is inline-first and engineered to run securely inside a TextEditingController. This imposes strict architectural rules:
- Single-pass, O(N) parsing: No AST, no look-behind, and no multi-pass resolution.
- The 1:1 Invariant: Every source character maps to exactly one cursor slot.
TextSpan-only layout: No composed widgets (likeColumn,Container, orPadding), relying entirely on Flutter'sText.richengine.
Because of these constraints, Textf does not implement all of CommonMark. Features that require multi-pass parsing (Reference Links) or box-model layouts (Lists with hanging indents) are strictly excluded by design.
Legend:
- ✅ Supported — Fully implemented and tested.
- 🚧 Coming in 2.0 — Architecturally viable and currently on the roadmap.
- ❌ Out of Scope — Conflicts with single-pass parsing or
TextSpanlayout constraints.
Inline Elements (Core) #
These elements map directly to Flutter's TextSpan and are the primary focus of Textf.
| Feature | Syntax / Example | Status | Notes |
|---|---|---|---|
| Textual Content | Plain text |
✅ | |
| Emphasis | *italic* or _italic_ |
✅ | |
| Strong Emphasis | **bold** or __bold__ |
✅ | |
| Code Spans | `inline code` |
✅ | |
| Inline Links | [text](https://url.com) |
✅ | Supports nested formatting inside link text |
| Backslash Escapes | \*literal asterisks\* |
✅ | Supported for all marker characters |
| Images |  |
❌ | Use {key} widget placeholders instead |
| Reference Links | [text][label] |
❌ | Requires multi-pass definition lookup |
| Autolinks | <https://url.com> |
❌ | Standard inline links [text](url) preferred |
| Raw HTML | <strong>text</strong> |
❌ | Not suitable for Flutter rendering |
Textf Extensions (GFM & Custom Inlines) #
Textf extends standard Markdown with highly requested inline features used in modern chat and document apps.
| Feature | Syntax / Example | Status | Notes |
|---|---|---|---|
| Strikethrough | ~~strike~~ |
✅ | |
| Underline | ++underline++ |
✅ | |
| Highlight | ==highlight== |
✅ | |
| Superscript | ^super^ |
✅ | |
| Subscript | ~sub~ |
✅ | |
| Widget Placeholders | {key} |
✅ | Renders embedded WidgetSpans seamlessly |
Block Elements #
Block elements are tightly restricted. Only elements that are forward-parsable on a single line are supported.
| Feature | Syntax / Example | Status | Notes |
|---|---|---|---|
| ATX Headings | # H1 to ###### H6 |
✅ | Up to 3 leading spaces, respects trailing # |
| Thematic Breaks | ---, ***, or ___ |
✅ | Renders as a full-width divider; customizable via TextfOptions.thematicBreakBuilder |
| Blockquotes | > quote text |
🚧 | Will be visually stylized (e.g., italics + color) rather than using a left-margin border |
| Lists (Unordered) | - item or * item |
❌ | TextSpan does not support hanging indents |
| Lists (Ordered) | 1. item |
❌ | TextSpan does not support hanging indents |
| Fenced Code Blocks | ```\ncode\n``` |
❌ | TextSpan backgrounds cannot be padded |
| Indented Code Blocks | code (4 spaces) |
❌ | Whitespace tracking is ambiguous in edit mode |
| Setext Headings | Heading\n=== |
❌ | Requires look-behind / retroactive restyling |
| Tables | ` | A | B |
When to Use Textf #
Textf is inline-first: inline formatting plus ATX headings as the one block-level exception. It is not a full 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 lists, tables, or blockquotes
- HTML rendering
- Block-level structure beyond ATX headings
Formatting Markers #
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++ |
||
| Highlight | ==highlight== |
highlight | |
| Inline code | `code` |
code |
|
| Superscript | ^super^ |
E = mc² | |
| Subscript | ~sub~ |
H₂O | |
| Link | [label](url) |
Flutter | |
| Placeholder | {key} |
(inserted widget) |
Flanking Rules #
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).
Nesting #
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`
Malformed or Unclosed Markers #
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)
Escaping #
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}
Textf Widget #
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.
Basic Usage #
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,
)
String Extensions #
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!"
Widget Placeholders #
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 #
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 Support #
SelectionArea(
child: Textf('Select **this** formatted text!'),
)
Performance #
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();
TextfEditingController #
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.
Limitations #
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
Basic Usage #
final controller = TextfEditingController();
TextField(controller: controller)
For headings in editable fields, disable forced strut height so bigger spans can own their line height:
final style = Theme.of(context).textTheme.bodyLarge!;
TextField(
controller: controller,
style: style,
strutStyle: StrutStyle.fromTextStyle(style, forceStrutHeight: false),
)
With initial content:
TextfEditingController(text: 'Hello **bold**')
Marker Visibility #
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;
Large Text Protection #
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: 5000
Custom Styles #
Wrap 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 #
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(),
)

Style Options #
| 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 |
Link Options #
| 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) |
Script Options #
| 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 |
How Inheritance Works #
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)'),
),
)
Theme Integration #
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.'),
)
Accessibility #
- 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
Comparison #
| 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 | Headings + rules | Full |
| Best for | Inline + headings + rules | Document rendering |
API Reference #
Full documentation on pub.dev.
AI Agent Skill #
Textf ships with an AI agent skill. Once you have textf as a dependency, run:
dart pub global activate skills
skills get
This 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.
Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.
License #
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.
