flutter_ai_chat_markdown

A rich, production-ready Markdown renderer for Flutter AI chat apps — built to match ChatGPT-quality output.

Supports streaming, LaTeX math, chemistry (mhchem), biology sequences, interactive charts, syntax-highlighted code blocks, tables, images with zoom, callout admonitions, and a fully customisable theme system.


Features

Category What's included
Text H1–H6, bold, italic, strikethrough, inline code, links
Lists Ordered, unordered, nested, task lists (- [x])
Code Syntax highlight (190+ languages), copy button, language label, collapse >50 lines
Table Horizontal scroll, zebra stripe, long-press to copy cell / row / TSV
Math Inline $...$ and block $$...$$ via KaTeX-compatible flutter_math_fork
Chemistry mhchem \ce{} — subscripts, arrows, ion charges, state symbols
Biology Color-coded DNA / RNA / protein sequences, GC% stat
Charts Bar, line, area, pie, scatter, radar from a JSON code fence
Images Remote/local with shimmer placeholder, tap-to-zoom with photo_view
Blockquote Standard quotes + 5 callout types: NOTE, WARNING, TIP, IMPORTANT, CAUTION
Streaming Throttled render (80 ms), stable block keys, blinking caret
Theme Immutable MarkdownTheme with chatGptLight / chatGptDark presets
Extensions Plug in any custom fence-tag renderer without touching core code

Installation

dependencies:
  flutter_ai_chat_markdown: ^0.1.0
flutter pub get

Quick start

import 'package:flutter_ai_chat_markdown/flutter_ai_chat_markdown.dart';

// Wrap your app once to provide the theme
MarkdownThemeScope(
  theme: MarkdownTheme.chatGptLight,
  child: MaterialApp(home: ChatScreen()),
)

// Then render anywhere
MarkdownRenderer(
  data: message.content,
  streaming: message.isStreaming,
  onTapLink: (url) => SafeLink.open(context, url),
)

Streaming

Pass streaming: true while tokens are arriving. The renderer throttles rebuilds to every 80 ms and only re-renders the incomplete tail block — stable blocks stay constant, so there is no full-widget reflow.

// Start streaming
MarkdownRenderer(data: partialMarkdown, streaming: true)

// When the response is complete
MarkdownRenderer(data: fullMarkdown, streaming: false)

Theme

Using a preset

MarkdownThemeScope(
  theme: MarkdownTheme.chatGptDark,
  child: child,
)

Customising a preset

final myTheme = MarkdownTheme.chatGptDark.copyWith(
  h1: const TextStyle(fontFamily: 'Lora', fontSize: 28, fontWeight: FontWeight.w700),
  codeBackground: const Color(0xFF161B22),
  linkColor: Colors.tealAccent,
  showCaret: true,
  caretColor: Colors.tealAccent,
);

Passing a theme directly to a widget

MarkdownRenderer(
  data: markdown,
  theme: myTheme,   // overrides MarkdownThemeScope
)

Theme tokens

Token Description
paragraph, h1h6 Typography for body and headings
code, blockquote, linkStyle Inline styles
tableHeader, tableCell Table typography
blockSpacing Vertical gap between blocks
codePadding, blockquotePadding, tableCellPadding Inner spacing
background, codeBackground, codeBackground Surface colours
blockquoteBar, tableBorder, tableHeaderBg, tableRowAltBg Component colours
linkColor, hrColor, mathColor, chemAtomColor Accent colours
codeRadius, tableRadius, imageRadius Corner radii
codeHighlightTheme Highlight.js theme name (e.g. 'atom-one-dark', 'github')
showCodeLanguageLabel, showCodeCopyButton Code block UI toggles
tokenFadeIn Fade duration for new streaming tokens
showCaret, caretColor Blinking caret while streaming

Supported Markdown syntax

Inline

**bold**   *italic*   ~~strikethrough~~   `inline code`
[link text](https://example.com)

Block elements

# H1 through ###### H6

- unordered list
1. ordered list
- [x] task list (checked)
- [ ] task list (unchecked)

> regular blockquote
> [!NOTE] informational callout
> [!WARNING] warning callout
> [!TIP] tip callout
> [!IMPORTANT] important callout
> [!CAUTION] caution callout

---   (horizontal rule)

Code blocks (syntax highlighted)

```python
def greet(name: str) -> str:
    return f"Hello, {name}!"
```

Supported: Python, Dart, JavaScript, TypeScript, Go, Rust, Java, Kotlin, Swift, C/C++, C#, SQL, Bash, YAML, JSON, HTML, CSS, and 180+ more via flutter_highlight.

Tables

| Header A | Header B | Header C |
|:---------|:--------:|---------:|
| left     | center   | right    |
| cell     | cell     | cell     |

Long-press any cell to copy the cell, row, whole table as TSV, or whole table as Markdown.


Math (LaTeX)

Inline: $a^2 + b^2 = c^2$

Block:

$$
\int_0^\infty e^{-x^2}\,dx = \frac{\sqrt{\pi}}{2}
$$

Supported commands: \frac, \sqrt, \sum, \int, \lim, \begin{pmatrix}, \binom, Greek letters, \mathbb, \mathcal, \cdot, \times, \to, \Rightarrow, and the full KaTeX-compatible subset from flutter_math_fork.

If the LaTeX fails to parse, the raw source is shown with a ⚠ indicator — the app never crashes.


Chemistry (mhchem)

Use \ce{} inside a math block:

$$\ce{2 H2 + O2 -> 2 H2O}$$

$$\ce{Fe^2+ + 2e- -> Fe}$$

$$\ce{CH3-CH2-OH}$$

The built-in MhchemTransform converts mhchem notation to plain LaTeX before passing it to flutter_math_fork — no WebView required.

Supported: subscripts, superscripts, arrows (->, <->, <=>), state symbols (s) (l) (g) (aq), ion charges.


Biology sequences

```bio:dna
ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG
```

```bio:rna
AUGGCCAUUGUAAUGGGCCGCUGAAAGGGUGCCCGAUAG
```

```bio:protein
MAIVMGRWKGAR*
```

Each nucleotide / amino acid is colour-coded (A = green, T/U = red, G = yellow, C = blue for nucleic acids; amino acids coloured by physicochemical group). A stats bar shows sequence length and GC% for DNA/RNA.


Charts

A chart fence takes a lightweight JSON spec:

```chart
{
  "type": "bar",
  "title": "Revenue Q1",
  "x": ["Jan", "Feb", "Mar"],
  "series": [
    { "name": "2025", "data": [120, 150, 170] },
    { "name": "2026", "data": [140, 160, 200] }
  ],
  "options": { "stacked": false, "legend": true }
}
```

Supported types: bar, line, area, pie, scatter, radar.

A "View data" button opens a bottom sheet with the raw data table. Invalid JSON shows a clear error message.


Images

![Alt text](https://example.com/image.png)
  • Remote images are fetched and cached via cached_network_image.
  • A shimmer placeholder is shown while loading; a retry button on failure.
  • Tap to open a full-screen photo_view hero with pinch zoom.
  • Alt text is used as the Semantics label.

All links go through SafeLink.open(context, url):

  • Only http, https, mailto, and tel schemes are allowed.
  • Unknown domains show a confirmation dialog ("You are about to open: example.com") before launching.

You can wire this up directly:

MarkdownRenderer(
  data: markdown,
  onTapLink: (url) => SafeLink.open(context, url),
)

Extension API

Register a custom fence-tag renderer without modifying the library:

class GeoMapExtension extends MarkdownExtension {
  @override
  String get fenceTag => 'geomap';

  @override
  Widget build(BuildContext context, String raw, MarkdownTheme theme) {
    return GeoMapWidget(geoJson: raw);
  }
}

MarkdownRenderer(
  data: markdown,
  extensions: [GeoMapExtension()],
)

Any ```geomap fence block is dispatched to your extension.


Accessibility

  • All text respects MediaQuery.textScalerOf(context) for system font scaling.
  • Semantics labels on code blocks ("Code block, python, 10 lines"), tables, and images.
  • Token fade-in animation is disabled when MediaQuery.disableAnimations is true.
  • Colours meet WCAG AA contrast in both light and dark presets.

Performance

Metric Target
Frame time during streaming (P95) ≤ 16 ms
Cold render 200-block message ≤ 250 ms
Memory for 50 KB message ≤ 8 MB

Key techniques:

  • Stable block keys prevent unnecessary rebuilds during streaming.
  • RepaintBoundary around code, math, and chart blocks.
  • Streaming rebuilds are local via ValueListenableBuilder, never at the root.
  • Images load lazily (only when within viewport ± 200 px).

API reference

MarkdownRenderer

Parameter Type Default Description
data String required Raw markdown string
theme MarkdownTheme? null Override for MarkdownThemeScope
selectable bool false Wrap content in SelectionArea
streaming bool false Enable streaming render mode
onTapLink void Function(String url)? null Link tap callback
imageBuilder Widget Function(BuildContext, String src)? null Custom image widget
extensions List<MarkdownExtension> [] Custom fence-tag renderers

MarkdownTheme

Static presets: MarkdownTheme.chatGptLight, MarkdownTheme.chatGptDark.

All fields are listed in the Theme tokens table above.

static Future<void> open(BuildContext context, String url)

ClipboardHelper

static Future<void> copyText(String text)
static Future<void> showCopySnackbar(BuildContext context, String message)

Example app

The example/ directory contains a full demo app with:

  • Demo screen — 25 sections showcasing every block type.
  • Chat screen — simulated AI streaming with preset responses covering math, code, tables, chemistry, and biology.

Run it:

cd example
flutter run

Roadmap

  • Mermaid diagrams (WebView-based, opt-in dependency)
  • Footnotes with popover
  • RTL (Arabic, Hebrew) support
  • Sticky table headers
  • TOC / scroll-to-section
  • HTML subset rendering (<sub>, <sup>, <kbd>, <mark>)

License

MIT — see LICENSE.

Libraries

flutter_ai_chat_markdown
A Flutter package for rendering AI-generated markdown with streaming support, math, chemistry, biology sequence, and chart visualizations.