A modular, framework-agnostic interactive text field for Flutter. Built on
Flutter's primitive EditableText — no Material or Cupertino dependency.
Behavior is contributed by plugins, so you can mix and match syntax
highlighting, markdown styling, slash-commands, mentions, code completion,
and live "iMessage" effects in any combination.
- Plugin-based. Decoration is contributed by independent
InteractiveTextPlugins. The controller merges them by priority into a singleTextSpantree — order-independent, override-able, composable. - Syntax highlighting for 13 hand-tuned grammars (Dart, JavaScript,
TypeScript, Python, Rust, Go, SQL, HTML, CSS, JSON, YAML, Bash,
Markdown). No transitive dependencies — register your own [
Grammar] for anything else. - Inline markdown styling without rendering a parallel widget tree —
**bold**,_italic_,`code`,[links](url), headings, lists, blockquotes. Delimiters dim so syntax stays visible, not noisy. - Regex highlighting for URLs, emails, hashtags, mentions, custom
patterns. Bring your own
RegExpandTextStyle. - Slash commands (
/help,/clear) and@mentionswith popup navigation, keyboard accept/cancel, custom row builders. - Inline code completion with a
CompletionProviderinterface — drop in a static list, hook up an LLM, or both viaCompositeCompletionProvider. - Live "iMessage" effects — short messages grow, long messages shrink, emoji-only messages get especially large, ALL-CAPS bolds up. Animated base-style transitions out of the box.
- Framework-neutral. Style with
Container,BoxDecoration, or your own design system. There is a runtime test guarding against accidentalmaterial.dart/cupertino.dartimports.
dependencies:
interactive_text_field:
git:
url: https://github.com/robertmollentze/interactive_text_fieldimport 'package:flutter/widgets.dart';
import 'package:interactive_text_field/interactive_text_field.dart';
final controller = InteractiveTextController(
plugins: [
MarkdownPlugin(),
RegexHighlightPlugin(rules: [
CommonRegexRules.url(),
CommonRegexRules.hashtag(),
]),
SyntaxHighlightPlugin(language: SyntaxLanguages.dart),
],
);
InteractiveTextField(
controller: controller,
maxLines: 8,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE0E0E0)),
),
style: const TextStyle(fontSize: 16, color: Color(0xFF1F1F1F)),
);Colors regex matches. Ships with CommonRegexRules for URLs, emails,
hashtags, and mentions.
RegexHighlightPlugin(rules: [
CommonRegexRules.url(),
RegexHighlightRule(
pattern: RegExp(r'\b(TODO|FIXME)\b'),
style: TextStyle(color: Color(0xFFD32F2F), fontWeight: FontWeight.bold),
),
]);13 built-in grammars (Dart, JavaScript/TypeScript, Python, Rust, Go,
SQL, HTML, CSS, JSON, YAML, Bash, Markdown). Two themes shipped
(githubLight, vsCodeDark); themes are just Map<String, TextStyle>,
so build your own. Register a custom Grammar to extend the registry.
SyntaxHighlightPlugin(
language: SyntaxLanguages.dart,
theme: SyntaxThemes.vsCodeDark,
);Inline-only — styles spans in place without rewriting them. Includes delimiter dimming.
MarkdownPlugin(style: MarkdownStyle.defaults);Both extend TriggerPlugin. Provide a list, the plugin handles detection,
filtering, popup rendering, and keyboard navigation.
SlashCommandPlugin(
commands: const [
SlashCommand(name: 'help', description: 'Show available commands'),
SlashCommand(name: 'clear', description: 'Clear the input'),
],
);
MentionPlugin(
mentions: [
Mention(id: '1', displayName: 'Rob', handle: 'rob', avatar: ...),
],
);For a custom trigger character (e.g. # for tags), instantiate
TriggerPlugin<T> directly.
Inline ghost-text. Press Tab to accept. The same UI works for static word lists or async LLM-backed completion.
CompletionPlugin(
provider: const StaticListCompletionProvider(['async', 'await', 'class']),
);
// Mix sync and async:
CompositeCompletionProvider([
const StaticListCompletionProvider([...]),
MyLlmCompletionProvider(),
]);Live form validation. Choose a trigger, supply a (String) → String?
validator, paint the error wherever you want.
final controller = InteractiveTextController(
plugins: [
ValidationPlugin(
validate: CommonValidators.compose([
CommonValidators.nonEmpty(),
CommonValidators.email(),
]),
),
],
);
final validation = controller.findPlugin<ValidationPlugin>()!;
ListenableBuilder(
listenable: validation.errorListenable,
builder: (_, __) => InteractiveTextField(
controller: controller,
hint: 'Email',
errorText: validation.error,
autofillHints: const [AutofillHints.email],
keyboardType: TextInputType.emailAddress,
),
);CommonValidators ships nonEmpty, email, password (with optional
uppercase/lowercase/digit/special rules), matches(RegExp), minLength,
maxLength, phoneE164, url, and compose([...]) to chain them.
Caps the controller at N characters (paste/IME/programmatic writes all honour the cap) and exposes a live counter.
final maxLen = MaxLengthPlugin(maxLength: 80);
final controller = InteractiveTextController(plugins: [maxLen]);
ListenableBuilder(
listenable: maxLen.currentLengthListenable,
builder: (_, __) => Text('${maxLen.currentLength}/${maxLen.maxLength}'),
);Password field with eye-toggle in one wiring. Host supplies the icon, so no icon pack is baked in.
final obscure = ObscureTextController();
InteractiveTextField(
controller: passwordController,
obscureTextNotifier: obscure,
autofillHints: const [AutofillHints.password],
hint: 'Password',
trailing: ObscureTextToggle(
notifier: obscure,
iconBuilder: (context, isObscured) => Icon(
isObscured ? Icons.visibility_off : Icons.visibility,
),
),
);Live, content-driven effects. Combine a BaseStyleResolver (global style
that depends on the entire text) with PatternEffects (range styles that
depend on a regex match).
EffectsPlugin(
baseStyleResolver: Effects.iMessageScale(),
effects: [
Effects.shouting(),
Effects.emojiPop(),
Effects.numbers(),
],
);InteractiveTextField smoothly animates the base style as it changes — so
when the user transitions from "hi" to a long paragraph the text shrinks
in a single transition instead of snapping.
Animation scope. Only the global base style is animated. Per-range style changes (e.g. a word becoming bold when its
**closes) apply immediately to keep the editor responsive to fast typing.
class HighlightAllAs extends InteractiveTextPlugin {
HighlightAllAs(this.style);
final TextStyle style;
@override
DecorationResult decorate(DecorationContext ctx) {
final ranges = <StyledRange>[];
for (var i = 0; i < ctx.text.length; i++) {
if (ctx.text[i].toLowerCase() == 'a') {
ranges.add(StyledRange(start: i, end: i + 1, style: style));
}
}
return DecorationResult(ranges);
}
}Plugins can also:
- Mutate text via
context.writeValue(...)— used byTriggerPluginandCompletionPluginto commit accepted suggestions. - Intercept keys via
onKeyEvent. - Contribute global style overrides via
overrideBaseStyle. - Show overlays (popups, suggestion menus) via
buildOverlay.
See example/ for one screen per feature: plain field, regex
highlighting, syntax highlighting, markdown, slash commands, mentions,
inline completion, message bubbles, and iMessage-style live effects.
cd example
flutter runflutter testThe package ships with widget tests, plugin unit tests, and a guard test
that fails the suite if any source file in lib/ imports material.dart
or cupertino.dart.
MIT.