Flutter Markdown: Render Markdown in Flutter Apps (2026)
September 11, 2026 · 9 min read
Flutter Markdown: Render Markdown in Flutter Apps
To render flutter markdown content in 2026, add flutter_markdown_plus to pubspec.yaml and drop a Markdown or MarkdownBody widget into your tree. Google's original flutter_markdown package was discontinued in 2025, and most tutorials still teach it. This one covers the migration, a complete working screen, and when to reach for markdown_widget or gpt_markdown instead.
What Happened to flutter_markdown?
On 10 February 2025 the Flutter team opened issue #162966 to announce that flutter_markdown was being discontinued. The issue invited the community to coordinate a fork of the Flutter Markdown package most apps depended on. The pub.dev page now carries a "discontinued" banner and names flutter_markdown_plus as the replacement. The last Google release was 0.7.7+1.
Which Flutter Markdown package should you use now? Four matter:
| Package | Version (Sept 2026) | Publisher | Best for |
|---|---|---|---|
flutter_markdown_plus | 1.0.12 | Foresight Mobile (verified) | Drop-in continuation; same API as the old package |
markdown_widget | 2.3.2+8 | morn.fun (verified) | Table of contents, LaTeX, dark mode config |
gpt_markdown | 1.2.1 | useval.io (verified) | Streaming AI chat output with LaTeX |
markdown | 7.3.1 | tools.dart.dev | The Dart parser underneath all of them |
The Dart markdown package does the parsing. It ships four extension sets: none, commonMark, gitHubFlavored (tables, strikethrough, autolinks), and gitHubWeb (adds header IDs and emoji). flutter_markdown_plus defaults to GitHub Flavored, which is why tables and ~~strikethrough~~ work without configuration.
Migrating from flutter_markdown to flutter_markdown_plus
The fork kept the class names, so migration is a dependency swap and an import change. In pubspec.yaml:
dependencies:
flutter_markdown_plus: ^1.0.12
url_launcher: ^6.3.2
Then replace the import everywhere:
// before
import 'package:flutter_markdown/flutter_markdown.dart';
// after
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
The classes Markdown, MarkdownBody, and MarkdownStyleSheet, plus the onTapLink and selectable parameters, all keep their names and signatures. The package describes itself as the continuation of the original with the same API, so for most apps the dependency swap is the whole migration. Run flutter pub get, rebuild, and diff a few screens against the old build to be sure.
Markdown vs MarkdownBody: Which Widget Do You Use?
The flutter_markdown_plus README draws the line clearly. Markdown is a standalone, scrollable Flutter Markdown widget: use it when the Markdown is the screen. MarkdownBody sizes itself to its content and doesn't scroll: use it when the rendered text sits inside your own layout, such as a ListView, a Card, or a chat bubble.
The most common mistake is putting Markdown inside a ListView. Two scrollables fight, and you get a viewport error or a widget that refuses to grow. Put MarkdownBody in the list instead and let the list scroll.
A Complete Flutter Markdown Screen
Here is one screen that renders a document with links, styled headings, and selectable text. It uses MarkdownBody inside a ListView so you can add other widgets around it.
import 'package:flutter/material.dart';
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
import 'package:url_launcher/url_launcher.dart';
const String source = '''
# Release notes
Version **2.4** ships two changes:
- Faster sync on [Android](https://flutter.dev)
- A fix for ~~duplicate~~ missing notifications
| Platform | Status |
|---|---|
| iOS | shipped |
| Android | rolling out |
''';
class NotesScreen extends StatelessWidget {
const NotesScreen({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: const Text('Release notes')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
MarkdownBody(
data: source,
selectable: true,
styleSheet: MarkdownStyleSheet.fromTheme(theme).copyWith(
h1: theme.textTheme.headlineMedium,
p: theme.textTheme.bodyLarge,
blockquoteDecoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
),
onTapLink: (text, href, title) async {
if (href == null) return;
final uri = Uri.parse(href);
if (!await launchUrl(uri)) {
debugPrint('Could not open $href');
}
},
),
],
),
);
}
}
Three parts do the work. selectable: true turns the whole body into selectable text, so users can copy a code sample. MarkdownStyleSheet.fromTheme(theme).copyWith(...) starts from your app theme and overrides only the elements you care about, which keeps dark mode working for free. onTapLink receives the link text, the href, and the title; nothing happens on tap until you handle it.
The link handler needs url_launcher 6.3.2 and one platform step. On Android 11 and later, declare the https scheme in a <queries> block in AndroidManifest.xml. On iOS, add LSApplicationQueriesSchemes to Info.plist if you open custom schemes. Skip that and launchUrl returns false with no visible error.
Write the Markdown in the Editor First
Author the document your app will show in a renderer that speaks the same dialect. Our editor renders GitHub Flavored Markdown, which matches flutter_markdown_plus defaults, so a table or task list that looks right here will look right in the widget. The snippet below is the release-notes document from the code above.
Ship the text as a string constant, an asset file, or a network response. For an asset, list the file under flutter: assets: in pubspec.yaml, load it once, and hand the string to the widget:
FutureBuilder<String>(
future: rootBundle.loadString('assets/notes.md'),
builder: (context, snapshot) {
if (!snapshot.hasData) return const CircularProgressIndicator();
return MarkdownBody(data: snapshot.data!);
},
)
The same pattern works for an HTTP response; keep the parsing on the widget side and the fetching in a service. For the syntax the widget understands, keep the Markdown cheat sheet open; the GFM column is the one that applies. Code blocks get a monospace style from code and codeblockDecoration in the style sheet, but no syntax colouring; add a builders entry with a highlighter package if you need that.
Images, HTML, and Other Things Flutter Renders Differently
A Flutter Markdown renderer builds native widgets, not a web page. The README says it plainly: "Flutter isn't an HTML renderer like a web browser," and the package doesn't support inline HTML. A <br> or <img> tag in your Markdown comes out as literal text. Use Markdown syntax for everything, and split paragraphs with a blank line instead of a break tag.
Images use standard  syntax. Per the README, the widget resolves three kinds of source: https:// URLs load over the network, resource: URIs load from your asset bundle, and plain paths load from the file system. Provide an imageBuilder if you want caching or placeholders. The Markdown image guide covers the syntax; the source-scheme rule is the Flutter-specific part.
One limitation to plan for: large documents render as one widget tree, so a 5,000-line changelog inside MarkdownBody builds every element at once. For very long content, split the Markdown into sections and render each in its own list item so ListView can lazily build them. On Flutter web the same rule applies, and selectable text over a very long body adds its own cost, so test with your real content rather than a lorem ipsum sample.
When to Use markdown_widget or gpt_markdown Instead
Pick markdown_widget when you need a table of contents that jumps to headings. It also brings LaTeX out of the box and a MarkdownConfig with separate light and dark styling. Its main widget is MarkdownWidget(data: data), and it lists Android, iOS, Linux, macOS, Web, and Windows as supported platforms. It hasn't had a release in over a year, so check open issues before adopting it.
Pick gpt_markdown when you're rendering streaming output from an AI model. Its GptMarkdown(reply, onLinkTap: ...) widget handles partial Markdown as tokens arrive, renders LaTeX, tables, and task lists, and the 1.2 release added an opt-in streaming reveal that rebuilds only the part of the reply that can still change. For LaTeX in flutter_markdown_plus itself, the maintainers point to a separate flutter_markdown_plus_latex package. The delimiters that survive each renderer are covered in our Markdown equation post.
We prefer flutter_markdown_plus as the default because it's the one most Stack Overflow answers still apply to, and switch only when a feature forces it. If you're rendering the same Markdown on a backend, the Python Markdown and Django Markdown posts show the server-side equivalents.
Common Flutter Markdown Mistakes
Nested scrolling. Markdown inside ListView, SingleChildScrollView, or a Column with Expanded. Fix: use MarkdownBody, or give Markdown the whole body with shrinkWrap: true only when you must.
Links that do nothing. onTapLink was never set, or launchUrl returns false because the Android <queries> block is missing. Fix: handle the callback and add the manifest entry, then test on a physical device, where the browser intent actually exists.
HTML in the source. Content copied from a CMS with <b> and <br> tags. Fix: convert it to Markdown once with the HTML to Markdown tool and store the result.
Flutter Markdown FAQ
Rendering flutter markdown in 2026 means flutter_markdown_plus for the default case, MarkdownBody inside your own layout, onTapLink wired to url_launcher, and gpt_markdown when the text streams from a model. Draft and check the content in the editor first, since it renders the same GFM dialect your widget will, and the surprises will be Flutter's rather than the Markdown's.