Basic Usage
Everything from install to a production-ready render, with GptMarkdown v1.2.1.
Install
1# pubspec.yaml
2dependencies:
3 gpt_markdown: ^1.2.11import 'package:gpt_markdown/gpt_markdown.dart';One import brings in the widget, every style class, all builder typedefs, and GptMarkdownConfig.
Minimal render
One positional argument — the markdown string. Everything else is optional.
1// Minimum: one positional argument — the markdown string.
2GptMarkdown('# Hello\n\nSome **bold** text and `inline code`.')What it supports
| Feature | Syntax or behavior |
|---|---|
| Headings | # through ###### |
| Emphasis | **bold**, *italic*, ~~strike~~, <u>underline</u> |
| Code | `inline` and fenced code blocks |
| Lists | Bulleted, ordered, nested, task lists, and radio options |
| Tables | GFM-style tables, including :---: alignment |
| Blocks | Quotes, rules, images, links, citations, and bare autolinks |
| Math | \( inline \) and \[ block \]; dollar signs are opt-in |
The package has no platform-specific plugins and is suitable for Flutter Web, including WebAssembly-targeted builds. Your app's other dependencies must still support the WebAssembly target.
Scrollable replies
GptMarkdown sizes itself to its content. Put it inside something scrollable for anything longer than a sentence — otherwise a long reply overflows the screen.1import 'package:flutter/material.dart';
2import 'package:gpt_markdown/gpt_markdown.dart';
3
4/// GptMarkdown sizes itself to its content and does not scroll.
5/// Wrap it whenever the reply may exceed the screen height.
6class ReplyView extends StatelessWidget {
7 final String reply;
8 const ReplyView({super.key, required this.reply});
9
10
11 Widget build(BuildContext context) {
12 return Scaffold(
13 appBar: AppBar(title: const Text('Reply')),
14 body: SingleChildScrollView(
15 padding: const EdgeInsets.all(16),
16 child: GptMarkdown(reply),
17 ),
18 );
19 }
20}In a chat list where the ListView already scrolls:
1// One GptMarkdown per bubble — no scroll wrapper needed
2// because the ListView itself scrolls.
3ListView.builder(
4 itemCount: messages.length,
5 itemBuilder: (context, i) => Padding(
6 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
7 child: GptMarkdown(messages[i].text),
8 ),
9)Text selection
Wrap in SelectionArea the same way you would any Flutter text. Inline code stays on the text baseline and is selectable. Copying across a list or table currently yields cells run together with no separators — prose, headings, links, and inline code copy correctly.
1// Wrap in SelectionArea to make all text inside selectable.
2SelectionArea(
3 child: SingleChildScrollView(
4 padding: const EdgeInsets.all(16),
5 child: GptMarkdown(reply),
6 ),
7)Handling link taps
Links render as tappable but do nothing on their own — opening a URL is your decision. LLM output can contain any URL, so validate before launching.
1// Links do nothing unless you handle them.
2// The package does not depend on a URL launcher — that choice is yours.
3GptMarkdown(
4 reply,
5 onLinkTap: (url, title) {
6 final uri = Uri.tryParse(url);
7 if (uri == null) return;
8 if (uri.scheme != 'https' && uri.scheme != 'mailto') return;
9 launchUrl(uri);
10 },
11)Image, code, citation, and checkbox callbacks
The remaining interactions are opt-in too. Markdown checkboxes are read-only by default; when enablingCheckboxStyle(interactive: true), persist a rewritten source string or the visible state will revert during the next rebuild.
1GptMarkdown(
2 reply,
3 onImageTap: (url) => openLightbox(url),
4 onCodeCopy: (code) => analytics.log('code_copied'),
5 onSourceTagTap: (content) => showSource(content),
6 // Requires CheckboxStyle(interactive: true).
7 onCheckboxChanged: (value) => persistCheckbox(value),
8)Right-to-left
Inline widgets — LaTeX, images, links — are placed in the correct visual order in mixed-direction paragraphs, working around flutter#54400 which the framework does not handle on its own.
1GptMarkdown(
2 reply,
3 textDirection: TextDirection.rtl,
4)Text scaling
Set the base font size once via style. Heading sizes, inline code, and list bullets all derive from it proportionally. Components rendered as inline widgets scale correctly at any system font setting.
1// Base text style — component sizes derive from this.
2// Set it once here; do not set sizes in individual style classes.
3GptMarkdown(
4 reply,
5 style: const TextStyle(fontSize: 16, height: 1.6),
6)
7
8// Or pull from the theme:
9GptMarkdown(
10 reply,
11 style: Theme.of(context).textTheme.bodyMedium,
12)Streaming AI output
Accumulate tokens in a StringBuffer, pass the string to GptMarkdown, and set isStreaming: true while the stream is open. The settled prefix is cached so cost per token stays flat regardless of reply length. See the Streaming guide for pacing, performance details, and pitfalls.
1// Streaming is data, not a Stream — rebuild with a longer string each token.
2class ReplyView extends StatefulWidget {
3 const ReplyView({super.key, required this.stream});
4 final Stream<String> stream;
5
6
7 State<ReplyView> createState() => _ReplyViewState();
8}
9
10class _ReplyViewState extends State<ReplyView> {
11 final _buffer = StringBuffer();
12 bool _generating = true;
13
14
15 void initState() {
16 super.initState();
17 widget.stream.listen(
18 (chunk) => setState(() => _buffer.write(chunk)),
19 onDone: () => setState(() => _generating = false),
20 );
21 }
22
23
24 Widget build(BuildContext context) => SingleChildScrollView(
25 padding: const EdgeInsets.all(16),
26 child: GptMarkdown(
27 _buffer.toString(),
28 animation: GptMarkdownAnimation.fade,
29 isStreaming: _generating, // flip to false when the stream ends
30 ),
31 );
32}LaTeX rendering
The package renders LaTeX by default. Use latexBuilder only when your app needs a different math widget or error treatment. See LaTeX Support for delimiters and display-math scroll.
1// LaTeX renders with the package default. Use latexBuilder only to replace it.
2// flutter_math_fork is the most common choice.
3import 'package:flutter_math_fork/flutter_math.dart';
4
5GptMarkdown(
6 reply,
7 latexBuilder: (context, tex, textStyle, inline) => Math.tex(
8 tex,
9 textStyle: textStyle,
10 onErrorFallback: (err) => Text(tex, style: textStyle),
11 ),
12)Custom code block
Use codeBuilder to replace the default code block widget. The closed flag is false while the closing fence has not yet arrived during streaming.
1// Replace the default code block widget with your own.
2GptMarkdown(
3 reply,
4 codeBuilder: (context, name, code, closed) {
5 // name = language identifier ("dart", "python", …) — may be empty
6 // code = raw code string
7 // closed = false while the closing fence hasn't arrived yet (streaming)
8 return MyCodeBlock(language: name, code: code, isClosed: closed);
9 },
10)Custom image renderer
The imageBuilder callback receives the URL plus optional width and height from the alt text (parsed as WxH).
1GptMarkdown(
2 reply,
3 imageBuilder: (context, url, width, height) {
4 return ClipRRect(
5 borderRadius: BorderRadius.circular(8),
6 child: Image.network(
7 url,
8 width: width,
9 height: height,
10 fit: BoxFit.cover,
11 ),
12 );
13 },
14)Custom link renderer
Use linkBuilder for complete control over how links are drawn. For tap-only needs, prefer the simpler onLinkTap.
1GptMarkdown(
2 reply,
3 linkBuilder: (context, text, url, style) {
4 return InkWell(
5 onTap: () => launchUrlString(url),
6 child: Text.rich(TextSpan(children: [text]),
7 style: style.copyWith(decoration: TextDecoration.underline)),
8 );
9 },
10)Common mistakes
Expanded without a scroll view. The widget reports its content height; constraining it with Expanded without a scroll view clips the reply. Always put a SingleChildScrollView between them.animation: none versus 11.0 ms with the split/cached fade path, which stays flat as the reply grows. Use the streaming guide for generated replies.latexBuilder, codeBuilder, etc.) are not compared when deciding whether to re-render, so a changed closure is silently ignored until the widget remounts. Define them once, outside build, or key the widget if you genuinely need to swap them.isStreaming: true after the reply finishes. The ticker keeps running and the tail keeps rebuilding for nothing. Always flip it on onDone — including error paths.Where next
Streaming
Accumulating text, animation modes, isStreaming lifecycle, pacing, reduced motion, and chat-list pitfalls.
Read moreMarkdown Features
Every supported construct — tables, task lists, citations, autolinks, scope and limitations.
Read moreLaTeX Support
Delimiters, dollar-sign opt-in, caller-provided renderer, horizontal scroll for wide equations.
Read moreSyntax Highlighting
Code block behaviour, custom codeBuilder, the closed flag, and selection caveats.
Read more