This guide helps you migrate from flutter_html (v3.x) and flutter_widget_from_html
(FWFH, v0.17.x) to HyperRender.
| Pain point | flutter_html | flutter_widget_from_html | HyperRender |
|---|---|---|---|
| HTML widgets per document | ~600 | ~500 | ~3–5 render chunks |
CSS float: left/right |
❌ Impossible | ❌ Impossible | ✅ |
| Text selection on large docs | ❌ Crashes (v0.17) | ✅ Crash-free | |
<ruby>/<rt> Furigana |
❌ Raw text | ❌ Not supported | ✅ |
<details>/<summary> |
❌ | ❌ | ✅ Interactive |
CSS Variables + calc() |
❌ | ❌ | ✅ |
| CSS Grid / Flexbox | ✅ Full |
If you need float, CJK typography, or crash-free selection — this migration
pays for itself immediately. If you need maximum CSS decoration coverage
and don't need those features, FWFH may still be appropriate.
// Before (flutter_html)
Html(data: htmlString)
// After (HyperRender)
HyperViewer(html: htmlString)// Before (FWFH)
HtmlWidget(htmlString)
// After (HyperRender)
HyperViewer(html: htmlString)Both libraries use positional String as their first argument. HyperRender
requires a named html: parameter — that is the only mandatory change.
// flutter_html
Html(
data: html,
onLinkTap: (url, _, __) => launchUrl(Uri.parse(url!)),
)
// FWFH
HtmlWidget(html, onTapUrl: (url) => launchUrl(Uri.parse(url)))
// HyperRender
HyperViewer(
html: html,
onLinkTap: (url) => launchUrl(Uri.parse(url)),
)HyperRender now supports a modular plugin system for rendering custom tags as
Flutter widgets. This is more robust than widgetBuilder for complex elements.
// flutter_html
Html(
data: html,
extensions: [TagExtension(tagsToExtend: {'my-card'}, builder: (ctx) => ...)],
)
// HyperRender
final registry = HyperPluginRegistry()
..register(MyCardPlugin()); // Implements HyperNodePlugin
HyperViewer(
html: html,
pluginRegistry: registry,
)// flutter_html
Html(
data: html,
extensions: [TagExtension(tagsToExtend: {'iframe'}, builder: (ctx) => ...)],
)
// FWFH
HtmlWidget(
html,
customWidgetBuilder: (element) {
if (element.localName == 'iframe') return MyWidget();
return null;
},
)
// HyperRender
HyperViewer(
html: html,
widgetBuilder: (context, node) {
if (node is AtomicNode && node.tagName == 'iframe') return MyWidget();
return null; // fall through to default rendering
},
)// flutter_html — custom style map
Html(
data: html,
style: {
'p': Style(fontSize: FontSize(16)),
'h1': Style(color: Colors.indigo),
},
)
// FWFH — custom stylesheet
HtmlWidget(html, customStylesBuilder: (element) {
if (element.localName == 'p') return 'font-size: 16px';
return null;
})
// HyperRender — inject a CSS string (full cascade, specificity respected)
HyperViewer(
html: html,
customCss: '''
p { font-size: 16px; line-height: 1.7; }
h1 { color: #3F51B5; }
a { color: #6750A4; }
''',
)// This just works in HyperRender. No migration needed — it was never possible before.
HyperViewer(
html: '''
<img src="photo.jpg" style="float: left; width: 200px; margin: 0 16px 8px 0;" />
<p>Text wraps naturally around the image — magazine style.</p>
''',
)// flutter_html — limited, no copy menu
Html(data: html, selectable: true)
// FWFH v0.17 — no built-in selection; wrapping in SelectionArea crashes on
// large documents because selection spans multiple independent RichText nodes
HtmlWidget(html, enableCaching: true) // no selection support
// HyperRender — crash-free, copy menu included
HyperViewer(
html: html,
selectable: true, // default: true
showSelectionMenu: true, // default: true
selectionHandleColor: Colors.blue,
)// flutter_html — no built-in sanitization
Html(data: userContent) // XSS risk
// FWFH — no built-in sanitization
HtmlWidget(userContent) // XSS risk
// HyperRender — sanitized by default
HyperViewer(html: userContent) // ✅ safe: sanitize: true is the default
// Opt-out only for fully trusted, backend-controlled HTML:
HyperViewer(html: trustedCmsHtml, sanitize: false)// HyperRender supports multiple formats — no separate package needed
HyperViewer.markdown(markdown: '# Hello\n\n**Bold** _italic_.')
HyperViewer.delta(delta: '{"ops":[{"insert":"Hello\\n"}]}')Be aware of these before migrating critical features:
| Feature | Status | Workaround |
|---|---|---|
position: absolute/fixed |
🚧 v4.0 planned | fallbackBuilder → WebView |
z-index stacking |
🚧 planned | n/a |
clip-path |
🚧 planned | n/a |
<canvas> |
❌ never (not a browser) | widgetBuilder injection |
<form>, <input> |
❌ | widgetBuilder injection |
| JavaScript execution | 🔮 v4.0 QuickJS (vanilla JS) | fallbackBuilder → WebView |
For complex HTML that uses these, use HtmlHeuristics to detect and
fall back to a WebView automatically:
HyperViewer(
html: maybeComplexHtml,
fallbackBuilder: (context) => WebViewWidget(controller: _webViewController),
)HyperViewer({
required String html,
String? baseUrl, // Resolve relative URLs
String? customCss, // Injected stylesheet (after document styles)
bool selectable = true,
bool sanitize = true,
List<String>? allowedTags, // Custom allowlist for sanitize: true
HyperRenderMode mode = HyperRenderMode.auto,
Function(String)? onLinkTap,
HyperWidgetBuilder? widgetBuilder,
WidgetBuilder? fallbackBuilder, // Called when HtmlHeuristics.isComplex()
GlobalKey? captureKey, // Screenshot export
bool enableZoom = false,
bool showSelectionMenu = true,
WidgetBuilder? placeholderBuilder,
String? semanticLabel,
HyperViewerController? controller,
HyperPageController? pageController, // Paged mode only
HyperPluginRegistry? pluginRegistry, // Custom tag plugins
void Function(Object, StackTrace)? onError,
})
HyperViewer.markdown(markdown: '# Hello', ...)
HyperViewer.delta(delta: jsonString, ...)Last updated: March 2026 — HyperRender v1.2.1