Pick the storage format before you pick the editor. The editor is a widget you can replace in an afternoon. The format is every document your users have already written, and replacing that means a migration script, a full backup, and a very careful evening.
Most teams do it backwards. Somebody needs a rich text field in a FlutterFlow app, searches the community forum, finds a Quill custom widget that looks good in the demo GIF, drops it in, wires the output to a Firestore string, and ships. Picture the moment: the toolbar works, bold is bold, and there is a field called body holding a blob of JSON. Nothing is wrong yet. Six weeks later somebody asks for search across all notes, and that is when the format shows up and presents its bill.
FlutterFlow's palette does not settle this for you. The built-in Markdown widget exposes two properties, Data and Selectable, and it renders text, it does not edit it. There is no native rich text editor in the widget tree at all. So every rich text feature in a FlutterFlow app starts as a custom widget decision, which means it is also a storage decision, and both get made in the same ten minutes by whoever is browsing packages.
I shipped five of these inside the Content Creator Toolkit bundle, part of eight FlutterFlow Marketplace templates and more than forty custom widgets: a Quill rich text editor producing Delta JSON, a drawing canvas, a markdown renderer, an HTML renderer, and a Fleather editor with PDF export. Five widgets, not one. They exist separately because these are five different problems wearing the same name.
Why does the storage format matter more than the editor?

The format decides what you can render, search, diff, sanitise and migrate for years. The editor only decides what typing feels like this week.
Think about what happens to a document after a user hits save. It gets rendered on a detail screen. It gets rendered again, smaller and plainer, in a list preview. Somebody searches for a phrase inside it. Somebody edits it and a teammate wants to see what changed between versions. Somebody exports it to PDF for a client. Much later, somebody asks why a document written on Android renders wrong on the web build.
Every one of those is a format operation. Not one of them is an editor operation. The editor finished its job the instant the user stopped typing, and it has no further opinion about the next three years.
This is why the question is not "which rich text editor is best for FlutterFlow". The honest question is which of those operations you will perform a thousand times a day, and which you will perform twice. Answer that and the format picks itself. If Flutter itself is new to you, the short version of what Flutter actually is will make the rest of this easier to follow.
What is Delta JSON, and when should you store it?

Delta is an operation log: an ordered list of insert, retain and delete instructions that rebuild a document when you replay them from the beginning.
The Quill Delta specification calls it "a strict subset of JSON" that is human readable and machine parsable. An insert carries text plus an optional attributes object holding the formatting. A retain advances past characters without changing them, and can apply formatting to the characters it passes over. A delete removes a count of characters without recording what those characters were, which is precisely why a Delta on its own is not reversible.
That op-log shape is not an accident. Delta was designed for Operational Transform, which is the machinery behind realtime, Google-Docs-style collaborative editing. If two people type into the same paragraph at the same moment, you need a format that can express "what changed" rather than "what it looks like now". Delta expresses exactly that.
flutter_quill, at version 11.6.0, is unambiguous about where the result should live: "We recommend storing the Document as Delta JSON instead of other formats (e.g., HTML, Markdown, PDF, Microsoft Word, Google Docs, Apple Pages, XML)." The reasoning is round-trip fidelity. Convert a Delta to HTML, store the HTML, convert it back later, and the structural mismatch between the two models quietly eats your formatting.
Here is the part that catches people. Delta is bad at search. A sentence with one bolded word in the middle is not one string in the JSON, it is three separate ops, so a query for "quarterly revenue report" will miss a document where "revenue" happens to be italic. Actually, "bad at search" overstates it. Delta is fine the moment you stop asking the storage format to also be the search index. Those are two jobs. Extract plain text at write time, store it in a second field, and index that instead, which is what you would end up doing on a FlutterFlow, Firebase and Stripe stack regardless of format.
HTML: the easy render and the hard sanitise

HTML renders in roughly one line of widget code and charges you a sanitisation pipeline in return. Anything a user can store, a user can aim at the next reader.
The render side really is easy. flutter_widget_from_html builds a Flutter widget tree from HTML with support for more than seventy of the most common tags, and its docs note that "SCRIPT and STYLE tags and their contents will be ignored". For a FlutterFlow app where users write and read their own content, that is often the end of the story.
It stops being the end of the story the moment a second reader exists. Ignoring script tags at render time is not the same as sanitising at write time. The stored string still contains whatever arrived, including event-handler attributes, javascript: URLs, and embedded iframes. Your Flutter renderer skips them. A web dashboard reading the same Firestore field will not. Neither will an email template, a PDF exporter, or the admin panel somebody builds next year.
If you are going to store HTML, sanitise on the way in. The sanitize_html package runs an HTML5 parser, builds an in-memory DOM tree, and filters elements and attributes against an allow-list, stripping inline JavaScript, CSS and form elements. Run it server-side, not only in the client, because the client is the part an attacker controls.
Markdown: the easy diff and the low ceiling
Markdown is plain text, so every tool you already own works on it unmodified: diff, grep, git, a Firestore text index, a human reading a log.
That is a real advantage and it is chronically undersold. A markdown document diffs line by line, which means a version history your users can actually read. Compare that to a Delta diff, which is structurally correct and almost impossible to render as "here is what your colleague changed" without building a translation layer first.
The ceiling is equally real. CommonMark 0.31.2, released in January 2024, is the closest thing to a settled definition of markdown, and what it defines is deliberately small: no tables, no text colour, no font size, no underline. Those arrive through extensions such as GitHub Flavored Markdown, and the moment your app depends on an extension you have stopped storing CommonMark and started storing a dialect that one particular renderer understands.
There is also a trapdoor. CommonMark specifies that "An HTML block is a group of lines that is treated as raw HTML (and will not be escaped in HTML output)". Allow raw HTML in your markdown and you have inherited every sanitisation problem from the previous section, with the added charm that it is now hidden inside a format people think of as safe.
Can you just store two formats?
Yes, and plenty of production apps do: Delta for fidelity, plus a rendered HTML or plain-text copy for everything that is not the editor.
The catch is that you now own a consistency problem. Two fields, one source of truth, and a write path that has to update both or neither. Worth it for search. Rarely worth it for rendering alone.
Delta, HTML and markdown across six operations
Here is the comparison laid out by operation rather than by feature list, because the operation is what you are actually choosing.
| Operation | Delta JSON | HTML | Markdown |
|---|---|---|---|
| Rendering | Needs the matching editor package to display faithfully | One widget, 70+ tags, minimal setup | One widget, small but predictable feature set |
| Full-text search | Needs a separate extracted plain-text field | Needs tag stripping before indexing | Indexable as-is, with minor syntax noise |
| Diffing | Structurally correct, hard to show a human | Noisy, tag churn swamps the text change | Line diff, readable by anyone |
| Sanitisation | Attribute allow-list over a known schema | Full HTML5 sanitiser required on write | Only required if raw HTML blocks are allowed |
| Portability | Quill and Fleather family, mainly | Universal, every platform reads it | Universal, every platform reads it |
| Migration cost | Cheap out to HTML, lossy coming back | Expensive into Delta, structure mismatch | Cheap either way, ceiling limits what carries |
Read down the column that matches your product. A collaborative notes app lives in the diffing and realtime rows, so Delta wins on merit. A CMS whose content is also served to a website lives in the rendering and portability rows, so HTML earns its sanitisation tax. A developer-facing app where users write release notes lives in the diffing and search rows, and markdown costs nothing.
What does PDF export demand from the format you chose?
PDF export needs an unambiguous mapping from your stored formatting to layout primitives, which favours formats with an explicit schema over formats that permit anything.
This is where storing loose HTML gets uncomfortable. HTML permits nested tables, floats, inline styles and arbitrary CSS, and your PDF generator has to make a decision about every one of them. Delta, by contrast, has a bounded attribute vocabulary. Bold, italic, list, heading level, link. A converter can enumerate the whole set and be done.
Fleather, at version 1.28.0, is the editor I paired with PDF export in the toolkit bundle. It uses the Parchment document model, which is itself built on the Quill.js Delta format, so it inherits the same bounded vocabulary and the same collaborative-editing readiness. The lineage runs Zefyr to Notus to Parchment, and knowing that lineage is genuinely useful when you are reading old Stack Overflow answers that use the previous names.
The renderer can be discontinued. The format has to outlive it.
Google discontinued flutter_markdown in 2025 and marked it discontinued on pub.dev, and the package that carried markdown rendering for most Flutter apps stopped being a first-party concern overnight.
The continuation is flutter_markdown_plus, now at version 1.0.12 and maintained by Foresight Mobile, and the deprecation thread is still open reading if you want to watch a package ecosystem reorganise itself in public. Teams that had stored markdown changed one dependency line. Nothing in their database moved.
That is the whole argument for boring formats, and it applies with more force inside FlutterFlow than outside it. A custom widget has to compile inside FlutterFlow's cloud build, and pub package constraints there are real. Packages that work in a plain Flutter project sometimes will not build in that environment, which narrows your renderer options further and makes renderer churn more expensive, not less. If you are weighing how far to go down this road, I wrote separately about when to drop out of FlutterFlow into pure Flutter custom code, and the package-availability question shows up there too.
Your documents should outlive your dependency list. Formats with a published specification and multiple independent implementations do that. Formats defined by whatever one package happens to emit do not.
How to pick, in order
You have a product in mind already. Work it in this order rather than starting from the editor demo.
- Write down the three operations your app will perform most on stored documents. Be specific: "render in a list preview", not "display text".
- If realtime collaborative editing is on that list, store Delta JSON and stop reading. Nothing else is close.
- If your content is also served outside the app, to a website, an email, or a public API, store HTML and budget a real server-side sanitiser before launch.
- If your users are technical, or if readable version history matters more than formatting range, store markdown and accept the ceiling.
- Whichever you chose, add a plain-text column at write time. It costs one line, it makes search possible, and it is the cheapest insurance against the next format migration.
- Only now pick the editor widget. Pick one that emits the format you already decided on, rather than deciding the format by picking the widget.
The free PDF Viewer template in my marketplace portfolio has roughly 1,500 downloads since it went live in May 2025, and building it taught me the same lesson this post is built on: the widget code was the short part. Deciding what the widget hands back, and what the next screen has to do with it, took longer than writing the renderer. If you are assembling the rest of the toolchain around that decision, the Flutter developer tools roundup covers the neighbouring choices.
So: open the file where your rich text is stored right now and look at the first document. Can you grep it? Can you diff it? Can you sanitise it? If the answer to all three is no, you did not choose a format. A widget chose one for you.
