Animated Markdown Renderer
A production-ready Flutter package for animated markdown rendering with ChatGPT-style typing animation, supporting customizable animation speed, chunk size, multiple animation modes, and an extensible custom widget system.
Features
- ✨ ChatGPT-style typing animation - Smooth, configurable typing effects
- 📡 Real-time streaming support - Stream markdown content as it arrives
- 🎨 Multiple animation modes - Character, word, token, or custom chunk-based
- 🎯 Preset configurations - Pre-configured styles matching popular AI assistants (ChatGPT, Claude, Grok, etc.)
- 🧩 Extensible custom widgets - Easy to add custom inline widgets with custom syntax patterns
- 📝 Full Markdown support - All standard markdown features
- 🎯 Inline widgets in lists - Custom widgets work seamlessly within list items
- ⚡ Performance optimized - Automatic throttling for large texts
- 🎛️ Fully configurable - Control speed, chunk size, and more
Installation
Add this to your package's pubspec.yaml file:
dependencies:
streaming_markdown: ^0.0.6
# For local development, use: streaming_markdown: { path: ../streaming_markdown }
Quick Start
import 'package:streaming_markdown/streaming_markdown.dart';
AnimatedMarkdown(
markdown: '''
# Hello World
This is **animated** markdown with [[button:Click Me]] inline!
- List item with [[chip:Tag]] inline widget
- Another item with [[button:Action]] button
- Nested list with [[chip:Nested]] chip
''',
config: AnimationConfig(
mode: AnimationMode.word,
wordDelay: Duration(milliseconds: 50),
chunkSize: 5,
),
customBuilders: {
'button': ButtonBuilder(),
'chip': ChipBuilder(),
},
)
Instant Rendering: Set shouldAnimate: false to render markdown instantly without animation.
Usage
Basic Usage
AnimatedMarkdown(
markdown: '# Hello\nThis is animated markdown!',
)
Instant Rendering
To render markdown instantly without animation, set shouldAnimate: false:
AnimatedMarkdown(
markdown: '# Hello\nThis is rendered instantly!',
shouldAnimate: false,
)
This is useful when you want to display content immediately or toggle between animated and instant rendering.
Custom Animation Configuration
AnimatedMarkdown(
markdown: 'Your markdown here',
config: AnimationConfig(
mode: AnimationMode.character, // or .word, .token, .custom
charDelay: Duration(milliseconds: 15),
wordDelay: Duration(milliseconds: 50),
tokenDelay: Duration(milliseconds: 30),
chunkSize: 1, // For batch rendering
throttleThreshold: 1000, // Performance optimization
),
)
Streaming Content
AnimatedMarkdown supports real-time streaming of markdown content via a Stream<String>. This is perfect for LLM responses, API streams, or any real-time content delivery. The widget automatically animates content as it arrives from the stream.
Basic Streaming:
Stream<String> markdownStream = getMarkdownStream();
AnimatedMarkdown(
stream: markdownStream,
config: AnimationConfig(
mode: AnimationMode.token, // Token mode is optimal for streaming
tokenDelay: Duration(milliseconds: 30),
),
)
With Custom Builders:
AnimatedMarkdown(
stream: markdownStream,
config: AnimationConfig.chatGPT,
customBuilders: {
'button': ButtonBuilder(),
'chip': ChipBuilder(),
},
)
Creating a Stream:
Stream<String> createMarkdownStream() {
final controller = StreamController<String>();
// Simulate streaming chunks
Future.microtask(() async {
final chunks = ['# Hello\n', 'This is **streamed** ', 'content!\n'];
for (final chunk in chunks) {
await Future.delayed(Duration(milliseconds: 100));
controller.add(chunk);
}
await controller.close();
});
return controller.stream;
}
The controller automatically handles stream completion and continues animating until all content is displayed. See the example/ directory for a complete streaming example.
Animation Complete Callback
You can be notified when the typing animation completes using the onAnimationComplete callback. This is useful for triggering actions after the animation finishes, such as showing a completion indicator or enabling user interactions.
With Static Markdown:
AnimatedMarkdown(
markdown: '# Hello\nThis is animated markdown!',
onAnimationComplete: (String finalText) {
print('Animation completed! Final text length: ${finalText.length}');
// Perform actions after animation completes
},
)
With Streaming Content:
AnimatedMarkdown(
stream: markdownStream,
onAnimationComplete: (String finalText) {
print('Stream animation completed!');
print('Total content: ${finalText.length} characters');
// Handle completion of streamed content
},
)
With Instant Rendering:
When shouldAnimate: false, the callback is invoked immediately after the content is rendered:
AnimatedMarkdown(
markdown: '# Instant content',
shouldAnimate: false,
onAnimationComplete: (String finalText) {
// Called immediately after rendering
print('Content rendered: $finalText');
},
)
Animation Modes
Character Mode
Smooth, character-by-character animation:
AnimationConfig(
mode: AnimationMode.character,
charDelay: Duration(milliseconds: 15),
)
Word Mode
Faster, word-by-word animation (default):
AnimationConfig(
mode: AnimationMode.word,
wordDelay: Duration(milliseconds: 50),
)
Token Mode
Optimal for LLM streaming:
AnimationConfig(
mode: AnimationMode.token,
tokenDelay: Duration(milliseconds: 30),
)
Custom Mode
Configurable chunk-based rendering:
AnimationConfig(
mode: AnimationMode.custom,
chunkSize: 10, // Characters per chunk
charDelay: Duration(milliseconds: 20),
)
Preset Configurations
AnimationConfig provides preset configurations that match popular AI assistant typing styles. These presets are optimized for different use cases:
ChatGPT Style: Character-based animation with smooth unit animations and slide effects:
AnimatedMarkdown(
markdown: 'Your content',
config: AnimationConfig.chatGPT,
)
Claude Style: Word-based animation with faster timing, optimized for quick responses:
AnimatedMarkdown(
markdown: 'Your content',
config: AnimationConfig.claude,
)
Grok Style: Fast token-based animation optimized for quick responses and real-time interactions:
AnimatedMarkdown(
markdown: 'Your content',
config: AnimationConfig.grok,
)
Perplexity Style: Fast word-based animation optimized for quick information delivery:
AnimatedMarkdown(
markdown: 'Your content',
config: AnimationConfig.perplexity,
)
Gemini Style: Moderate word-based animation with slightly slower pacing:
AnimatedMarkdown(
markdown: 'Your content',
config: AnimationConfig.gemini,
)
Copilot Style: Very fast token-based animation optimized for code generation and technical content:
AnimatedMarkdown(
markdown: 'Your content',
config: AnimationConfig.copilot,
)
All Available Presets:
AnimationConfig.chatGPT- Character mode with smooth animationsAnimationConfig.claude- Word mode with fast timingAnimationConfig.grok- Token mode for quick responsesAnimationConfig.perplexity- Word mode for information deliveryAnimationConfig.gemini- Word mode with moderate pacingAnimationConfig.copilot- Token mode for code generation
Custom Widgets
Inline Widgets
Inline widgets work within text and list items:
Buttons:
AnimatedMarkdown(
markdown: 'Click [[button:Click Me]] here',
customBuilders: {
'button': ButtonBuilder(
onPressed: (label) => print('Button pressed: $label'),
),
},
)
Chips:
AnimatedMarkdown(
markdown: 'Tag: [[chip:Important]]',
customBuilders: {
'chip': ChipBuilder(
onPressed: (label) => print('Chip pressed: $label'),
color: Colors.blue,
),
},
)
In List Items:
AnimatedMarkdown(
markdown: '''
- Item with [[button:Action]] button
- Another with [[chip:Tag]] chip
- Nested with [[button:Nested]] button
''',
customBuilders: {
'button': ButtonBuilder(),
'chip': ChipBuilder(),
},
)
Manual Control
Control animation by setting autoStart: false and using a MarkdownTypingController directly:
final controller = MarkdownTypingController(
fullText: 'Your text',
config: AnimationConfig(mode: AnimationMode.word),
);
// Start animation
controller.start();
// Control animation
controller.pause();
controller.resume();
controller.stop();
controller.reset();
controller.jumpToEnd();
Creating Custom Builders
The package provides a flexible builder system for creating custom markdown widgets. There are two base classes you can extend:
CustomMarkdownBuilder
The base class for all custom builders. Use this when you need full control over widget building:
class MyCustomBuilder extends CustomMarkdownBuilder {
@override
Widget? buildWidget(md.Element element, TextStyle? style) {
return Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Text(element.textContent),
);
}
}
InlineBuilder
Extend InlineBuilder for inline widgets that appear within text and list items. This is the recommended approach for most custom widgets:
class MyInlineBuilder extends InlineBuilder {
@override
Widget buildInline(md.Element element, TextStyle? style) {
return Container(
padding: EdgeInsets.all(4),
child: Text(element.textContent),
);
}
}
Custom Syntax Patterns
By default, custom builders use the pattern [[key:content]]. You can customize this pattern using the customSyntaxPatterns parameter:
AnimatedMarkdown(
markdown: 'Citation [1] and [2] in text',
customBuilders: {
'citation': CitationBuilder(),
},
customSyntaxPatterns: {
'citation': r'\[(\d+)\]', // Custom regex pattern
},
)
The custom pattern should include at least one capture group to extract the content. The first capture group will be used as the content.
Example: Citation Builder with Custom Pattern
class CitationBuilder extends InlineBuilder {
final Function(String)? onPressed;
CitationBuilder({this.onPressed});
@override
Widget buildInline(md.Element element, TextStyle? style) {
return GestureDetector(
onTap: () => onPressed?.call(element.textContent),
child: Text(
'[${element.textContent}]',
style: style?.copyWith(
color: Colors.blue,
decoration: TextDecoration.underline,
),
),
);
}
}
// Usage with custom pattern
AnimatedMarkdown(
markdown: 'This is content with citation [1] and [2].',
customBuilders: {
'citation': CitationBuilder(
onPressed: (number) => print('Citation $number clicked'),
),
},
customSyntaxPatterns: {
'citation': r'\[(\d+)\]', // Matches [1], [2], etc.
},
)
Custom Syntax (Advanced)
For more complex syntax requirements, you can create a custom syntax parser:
class MyInlineSyntax extends CustomInlineSyntax {
MyInlineSyntax() : super(RegExp(r'\[\[mywidget:(.*?)\]\]'));
@override
md.Node parseMatch(md.InlineParser parser, Match match) {
final String content = match.group(1)?.trim() ?? '';
return md.Element.text('mywidget', content);
}
}
However, for most use cases, using customSyntaxPatterns with InlineBuilder is simpler and recommended.
Registering Builders
Register your custom builders in AnimatedMarkdown or MarkdownRenderer:
AnimatedMarkdown(
markdown: 'Content with [[mywidget:Custom Content]]',
customBuilders: {
'mywidget': MyInlineBuilder(),
},
customSyntaxPatterns: {
'mywidget': r'\[\[mywidget:(.*?)\]\]', // Optional custom pattern
},
)
API Reference
AnimatedMarkdown
Main widget for animated markdown rendering.
Properties:
markdown(String?) - Markdown text to animate (eithermarkdownorstreammust be provided)stream(Streamconfig(AnimationConfig?) - Animation configurationcustomBuilders(Map<String, CustomMarkdownBuilder>?) - Custom widget builderscustomSyntaxPatterns(Map<String, String>?) - Custom regex patterns for builders. Maps builder keys to their regex patterns. If a builder key has a custom pattern, it will be used instead of the default[[key:content]]patternselectable(bool) - Whether text is selectable (default: true)styleSheet(MarkdownStyleSheet?) - Custom markdown style sheetextensionSet(ExtensionSet?) - Custom extension set for markdown parsing. If not provided, uses GitHub Flavored Markdown as defaultsyntaxHighlighter(SyntaxHighlighter?) - Syntax highlighter for code blocksonTapLink(MarkdownTapLinkCallback?) - Callback for handling link tapsshrinkWrap(bool) - Whether the widget should take the minimum height that wraps its content (default: true)softLineBreak(bool) - Whether to handle soft line breaks (default: false)autoStart(bool) - Whether to auto-start animation (default: true)shouldAnimate(bool) - Whether to animate the markdown text. Whenfalse, the markdown is rendered instantly without animation (default: true)onAnimationComplete(void Function(String)?) - Callback invoked when the typing animation completes. Receives the final animated text as a parameter. Works with both static markdown and streaming content
AnimationConfig
Configuration for typing animation.
Properties:
mode(AnimationMode) - Animation mode (default: AnimationMode.word)charDelay(Duration) - Delay between characters (default: 15ms)wordDelay(Duration) - Delay between words (default: 50ms)tokenDelay(Duration) - Delay between tokens (default: 30ms)chunkSize(int) - Chunk size for batch rendering (default: 1)throttleThreshold(int) - Threshold for performance throttling (default: 1000)
MarkdownTypingController
Controller for managing typing animation. Supports both static text and streaming content.
Constructor:
MarkdownTypingController({String? fullText, Stream<String>? stream, AnimationConfig? config})- Creates a controller with either static text (fullText) or a stream (stream). Exactly one must be provided.
Methods:
start()- Start the animationpause()- Pause the animationresume()- Resume the animationstop()- Stop the animationreset()- Reset to beginningjumpToEnd()- Jump to end immediately
Properties:
fullText(String) - Full text to animate (accumulated text from stream or initial text)text(String) - Current displayed textisRunning(bool) - Whether animation is runningisPaused(bool) - Whether animation is pausedprogress(double) - Progress (0.0 to 1.0)isComplete(bool) - Whether animation is complete (for streams, checks if stream is completed)
MarkdownRenderer
Renders markdown with custom builders and syntaxes.
Properties:
data(String, required) - Markdown text to rendercustomBuilders(Map<String, CustomMarkdownBuilder>?) - Custom widget builderscustomSyntaxPatterns(Map<String, String>?) - Custom regex patterns for builders. Maps builder keys to their regex patterns. If a builder key has a custom pattern, it will be used instead of the default[[key:content]]patternselectable(bool) - Whether text is selectable (default: true)styleSheet(MarkdownStyleSheet?) - Custom markdown style sheetanimationConfig(AnimationConfig?) - Animation configuration for unit-level animationsextensionSet(ExtensionSet?) - Custom extension set for markdown parsing. If not provided, uses GitHub Flavored Markdown as defaultsyntaxHighlighter(SyntaxHighlighter?) - Syntax highlighter for code blocksonTapLink(MarkdownTapLinkCallback?) - Callback for handling link tapsshrinkWrap(bool) - Whether the widget should take the minimum height that wraps its content (default: true)softLineBreak(bool) - Whether to handle soft line breaks (default: false)
Built-in Builders
ButtonBuilder- Inline button widgetChipBuilder- Inline chip/tag widgetMathBuilder- Math/LaTeX widget (placeholder)
Builder Base Classes
CustomMarkdownBuilder- Base class for all custom markdown buildersInlineBuilder- Base class for inline widgets that appear within text and list items
Performance Tips
- Use word mode for faster animation on large texts
- Increase chunk size for batch rendering
- Set throttleThreshold to enable automatic throttling for very long texts
- Disable autoStart and control manually for better performance
Examples
See the example/ directory for complete examples.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License.