Skip to content
Tech Stack21 September 2026 · 12 min read

Storing Rich Text in FlutterFlow: Delta, HTML, Markdown

Pick the storage format before you pick the editor. Delta JSON, HTML and markdown each lock you into a different future, and swapping later means migrating every document you already have.

Storing Rich Text in FlutterFlow: Delta, HTML, Markdown

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?

A blank cream sheet of paper threaded through a black metal lattice overgrown with sage vines, an old keyboard half-buried in moss at the bottom of the frame

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?

A segmented green vine growing along a dark circuit board trace, each joint clearly visible, tipped with a small glowing cyan bud, with dust in the grooves and a dried brown leaf at its base

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

Pale cream water pouring through a fine dark mesh set into an illustrated circuit board, sage leaves passing cleanly through while sharp black shards pile up on the buckled mesh

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.

OperationDelta JSONHTMLMarkdown
RenderingNeeds the matching editor package to display faithfullyOne widget, 70+ tags, minimal setupOne widget, small but predictable feature set
Full-text searchNeeds a separate extracted plain-text fieldNeeds tag stripping before indexingIndexable as-is, with minor syntax noise
DiffingStructurally correct, hard to show a humanNoisy, tag churn swamps the text changeLine diff, readable by anyone
SanitisationAttribute allow-list over a known schemaFull HTML5 sanitiser required on writeOnly required if raw HTML blocks are allowed
PortabilityQuill and Fleather family, mainlyUniversal, every platform reads itUniversal, every platform reads it
Migration costCheap out to HTML, lossy coming backExpensive into Delta, structure mismatchCheap 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.

  1. Write down the three operations your app will perform most on stored documents. Be specific: "render in a list preview", not "display text".
  2. If realtime collaborative editing is on that list, store Delta JSON and stop reading. Nothing else is close.
  3. 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.
  4. If your users are technical, or if readable version history matters more than formatting range, store markdown and accept the ceiling.
  5. 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.
  6. 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.

Free resource

Free SaaS MVP Scope Template

A Notion document with the full feature checklist, MVP vs. nice-to-have table, pre-build questions, and cost signals — so you walk into any developer call knowing exactly what to ask for.

Get the template →
DL

Dusko Licanin

Full-Stack Developer · Banja Luka, Bosnia

Full-stack developer shipping SaaS MVPs, web apps, and mobile apps using AI-augmented workflows — without agency coordination overhead. Live portfolio: BookBed, Callidus, Pizzeria Bestek.

Frequently Asked Questions

What is Delta JSON in Flutter Quill?

Delta JSON is an ordered list of insert, retain and delete operations that rebuild a rich text document when replayed from the start. An insert adds text along with an attributes object holding its formatting, a retain skips past characters and can apply formatting to the ones it passes, and a delete removes a count of characters without recording what they were. That last detail means a Delta is not reversible on its own. The shape exists because Delta was designed for Operational Transform, the mechanism behind realtime collaborative editing, so it expresses what changed rather than what the document currently looks like.

Does FlutterFlow have a built-in markdown renderer?

Yes. FlutterFlow ships a Markdown widget in the Base Elements section of the widget palette, and it displays markdown-formatted text without any custom code. Its documented properties are minimal: Data, where the markdown string goes, and Selectable, which controls whether users can highlight the text. What it does not do is edit. There is no native rich text editor widget in FlutterFlow, so anything with a toolbar is a custom widget you add yourself, which is also the moment you choose a storage format whether you realise it or not.

What is the best storage format for rich text in Flutter?

There is no single best format, but there is a reliable way to choose: pick the one that makes your most frequent operation cheap. Realtime collaborative editing points at Delta JSON, since flutter_quill explicitly recommends storing documents as Delta rather than HTML or markdown. Content that is also served to a website or an email points at HTML, with a real sanitiser on the write path. Readable version history and simple search point at markdown. Whichever you pick, store an extracted plain-text copy alongside it. Search needs it, and the next migration will thank you.

How do you render HTML in a FlutterFlow app?

You render HTML through a custom widget wrapping a Flutter HTML package, because FlutterFlow has no native HTML widget. flutter_widget_from_html is the common choice: it builds a widget tree from HTML with support for more than seventy tags, and it ignores SCRIPT and STYLE tags and their contents. Ignoring those at render time is not sanitising, though. The stored string still holds whatever was written, so sanitise server-side on write using something like sanitize_html before any other consumer, a web dashboard or an export job, reads the same field.

Can you migrate stored HTML to Delta JSON later?

You can, but it is the expensive direction and it loses information. HTML permits nested structures, inline styles and arbitrary CSS that have no equivalent in Delta's bounded attribute vocabulary, so the converter has to drop or approximate anything outside that set. Going the other way, Delta to HTML, is comparatively clean because you are widening rather than narrowing. Plan for this before launch rather than after: decide which direction you might need, and if the answer is uncertain, store plain text alongside whatever you chose so a future migration has an unambiguous fallback for every document.