The only Flutter HTML renderer with CSS float layout.
Renders HTML, Markdown, and Quill Delta using a single custom RenderObject — not a widget tree.
Drop-in replacement for flutter_html and flutter_widget_from_html. Ships with CSS float, Flexbox, Grid, CJK typography, crash-free text selection, and zero JS dependencies.
Quick Start · Why Switch? · Features · API · Benchmarks
dependencies:
hyper_render: ^1.1.0import 'package:hyper_render/hyper_render.dart';
// Works. Sanitization is ON by default — safe for user content.
HyperViewer(
html: articleHtml,
onLinkTap: (url) => launchUrl(Uri.parse(url)),
)That's it. No configuration required. Works for news articles, emails, documentation, and CJK content out of the box.
Most Flutter HTML libraries map each tag to a Flutter widget — Column, Row, Padding, Wrap, RichText. A 3,000-word article becomes 500+ deeply nested widgets, and then:
| What you need | flutter_html / FWFH | HyperRender |
|---|---|---|
float: left/right |
❌ Impossible | ✅ |
| Text selection (large docs) | ❌ Crashes | ✅ Crash-free |
| Ruby / Furigana | ❌ Shows raw text | ✅ |
<details>/<summary> |
❌ | ✅ Interactive |
CSS Variables var() |
❌ | ✅ |
| Flexbox / Grid | ✅ Full | |
| Box shadow & filters | ❌ | ✅ |
| Scroll FPS (25K-char article) | ~35–45 fps | 60 fps |
| RAM usage | 15–28 MB | ~8 MB |
Float is not a missing feature you can add. To wrap text around a floated image, the layout engine needs every fragment's coordinates before composing the adjacent text. A widget tree where each Column owns its own layout simply cannot share that geometry — there is no algorithm that fixes this without replacing the widget tree entirely.
That is exactly what HyperRender does: one RenderObject, one coordinate system, one layout pass.
HyperViewer(
html: '''
<article>
<img src="photo.jpg"
style="float: left; width: 180px; margin: 0 16px 8px 0; border-radius: 8px;" />
<h2>Magazine Layout</h2>
<p>This text flows around the floated image — exactly like a real browser.
No other Flutter library can render this correctly.</p>
</article>
''',
)HyperViewer(
html: longArticleHtml,
selectable: true, // default
showSelectionMenu: true, // Copy / Select All
selectionHandleColor: Colors.blue,
)One continuous span tree = selection across headings, paragraphs, and table cells. Tested to 100,000-character documents in CI without crashes.
HyperViewer(
html: '''
<p style="font-size: 20px; line-height: 2;">
<ruby>東京<rt>とうきょう</rt></ruby>で
<ruby>日本語<rt>にほんご</rt></ruby>を学ぶ
</p>
''',
)Furigana renders centered above base characters. Kinsoku shori (line-breaking rules) applied across the full line — not truncated at widget boundaries like every other library.
// CSS custom properties
HyperViewer(html: '''
<style>
:root { --brand: #6750A4; --gap: 16px; }
.card { background: var(--brand); padding: calc(var(--gap) * 1.5);
border-radius: 12px; color: white; }
</style>
<div class="card">Themed with CSS custom properties</div>
''')
// Flexbox navigation bar
HyperViewer(html: '''
<div style="display: flex; justify-content: space-between;
align-items: center; gap: 16px; background: #1976D2;
padding: 12px; border-radius: 8px; color: white;">
<strong>MyApp</strong>
<div style="display: flex; gap: 20px;">
<span>Home</span><span>Blog</span><span>About</span>
</div>
</div>
''')
// CSS Grid
HyperViewer(html: '''
<div style="display: grid; grid-template-columns: 1fr 2fr 1fr; gap: 12px;">
<div style="background: #E3F2FD; padding: 16px;">Sidebar</div>
<div style="background: #F3E5F5; padding: 16px;">Main</div>
<div style="background: #E8F5E9; padding: 16px;">Aside</div>
</div>
''')// HTML
HyperViewer(html: '<h1>Hello</h1><p>World</p>')
// Quill Delta JSON
HyperViewer.delta(delta: '{"ops":[{"insert":"Hello\\n"}]}')
// Markdown (CommonMark)
HyperViewer.markdown(markdown: '# Hello\n\n**Bold** and _italic_.')
// Custom CSS injection
HyperViewer(
html: articleHtml,
customCss: 'body { font-size: 18px; line-height: 1.8; } a { color: #6750A4; }',
)XSS protection is on by default. You opt out, not in.
// ✅ Safe by default — strips <script>, on* handlers, javascript: URLs
HyperViewer(html: userGeneratedContent)
// ✅ Custom allowlist
HyperViewer(
html: userContent,
allowedTags: ['p', 'a', 'img', 'strong', 'em', 'ul', 'li'],
)
// ⚠️ Disable only for trusted internal HTML
HyperViewer(html: trustedCmsHtml, sanitize: false)Blocked by default: <script> · <iframe> · on* event handlers · javascript: URLs · vbscript: · data:image/svg+xml · CSS expression()
// Automatic — HtmlHeuristics detects complex HTML and routes it to WebView
HyperViewer(
html: maybeComplexHtml,
fallbackBuilder: (context) => WebViewWidget(controller: _webViewController),
)
// Manual check
if (HtmlHeuristics.isComplex(html)) {
// Use WebView
}Because HyperRender controls the paint cycle directly, it supports effects that standard Flutter widgets make difficult:
- Glassmorphism:
backdrop-filter: blur(10px) - Box & text shadows: multi-layer, with spread and blur
- CSS filters:
filter: blur(4px) brightness(1.2) contrast(0.8) - Gradients:
background: linear-gradient(to right, #6a11cb, #2575fc) - Dashed / dotted borders:
border-style: dashed | dotted - High-DPI images: automatic
FilterQuality.medium
// Built-in Unicode renderer (zero deps)
FormulaWidget(formula: r'E = mc^2')
FormulaWidget(formula: r'\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}')
// Plug in flutter_math_fork for full LaTeX
FormulaWidget(
formula: r'\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}',
customBuilder: (context, formula) => Math.tex(formula),
)Also works as a Quill Delta embed: {"insert": {"formula": "E = mc^2"}}.
final captureKey = GlobalKey();
HyperViewer(html: articleHtml, captureKey: captureKey)
final pngBytes = await captureKey.toPngBytes();
final image = await captureKey.toImage();<details>
<summary>Click to expand</summary>
<p>HyperRender is the only Flutter HTML library that supports this interactively.</p>
</details><p dir="rtl">هذا نص عربي من اليمين إلى اليسار</p>
<p dir="ltr">Back to left-to-right text.</p>Measured on iPhone 13 (iOS 17) + Pixel 6 (Android 13) with a 25,000-character article.
Run flutter run --release benchmark/performance_test.dart to reproduce.
| Metric | flutter_html | flutter_widget_from_html | HyperRender |
|---|---|---|---|
| Flutter widgets created | ~600 | ~500 | 3–5 chunks |
| Parse time | 420 ms | 250 ms | 95 ms |
| RAM usage | 28 MB | 15 MB | 8 MB |
| Scroll FPS | ~35 | ~45 | 60 |
float: left/right |
❌ | ❌ | ✅ |
| Selection on large docs | ❌ Crashes | ✅ | |
| Ruby / Furigana | ❌ | ❌ | ✅ |
| CSS Variables | ❌ | ❌ | ✅ |
| Glassmorphism / Filters | ❌ | ❌ | ✅ |
"Widgets created": flutter_html / FWFH produce one Flutter widget per HTML tag. HyperRender splits the document into ~3–5
RenderHyperBoxchunks and paints each directly to Canvas — the tag count never maps to widget count.
HyperViewer({
required String html,
String? baseUrl, // Resolve relative URLs
String? customCss, // Inject extra CSS
bool selectable = true, // Enable text selection
bool sanitize = true, // XSS protection (default: ON)
List<String>? allowedTags, // Custom sanitizer allowlist
HyperRenderMode mode = HyperRenderMode.auto, // sync | virtualized | auto
Function(String)? onLinkTap,
HyperWidgetBuilder? widgetBuilder, // Inject Flutter widgets by tag
WidgetBuilder? fallbackBuilder, // Shown when HtmlHeuristics.isComplex()
WidgetBuilder? placeholderBuilder, // Async loading state
GlobalKey? captureKey, // Screenshot export
bool enableZoom = false,
bool showSelectionMenu = true,
String? semanticLabel,
HyperViewerController? controller, // Programmatic scroll + anchor jump
void Function(Object, StackTrace)? onError,
})
HyperViewer.delta(delta: jsonString, ...)
HyperViewer.markdown(markdown: markdownString, ...)HyperViewer(
html: htmlContent,
widgetBuilder: (context, node) {
if (node is AtomicNode && node.tagName == 'iframe') {
final src = node.attributes['src'] ?? '';
if (src.contains('youtube.com')) return YoutubePlayer(url: src);
}
return null; // fall through to default rendering
},
)final _ctrl = HyperViewerController();
HyperViewer(html: articleHtml, controller: _ctrl)
// Jump to any #anchor in the document
_ctrl.jumpToAnchor('section-2');
_ctrl.scrollToOffset(1200);HtmlHeuristics.isComplex(html) // any of the below
HtmlHeuristics.hasComplexTables(html) // colspan > 3 or deeply nested tables
HtmlHeuristics.hasUnsupportedCss(html) // position:fixed, clip-path
HtmlHeuristics.hasUnsupportedElements(html) // canvas, form, select, inputSmartTableWrapper(
tableNode: myTableNode,
strategy: TableStrategy.horizontalScroll, // fitWidth | autoScale | horizontalScroll
minScaleFactor: 0.6,
)FormulaWidget(
formula: r'\frac{-b \pm \sqrt{b^2-4ac}}{2a}',
style: TextStyle(fontSize: 18),
customBuilder: (context, f) => Math.tex(f), // optional flutter_math_fork hook
)HTML / Delta / Markdown
│
▼
ADAPTER LAYER ← HtmlAdapter · DeltaAdapter · MarkdownAdapter
│
▼
UNIFIED DOCUMENT TREE ← BlockNode · InlineNode · AtomicNode · RubyNode
TableNode · FlexContainerNode · GridNode
│
▼
CSS RESOLVER ← specificity cascade · var() · calc() · inheritance
│
▼
SINGLE RenderObject ← BFC · IFC · Flexbox · Grid · Table · Float
Canvas painting · continuous span tree
Kinsoku · perfect text selection
Key design choices:
- Single RenderObject — the whole document is one
RenderBox; float layout and crash-free selection are only possible because every fragment's coordinates live in one coordinate system - O(1) CSS rule lookup — rules are indexed by tag/class/ID; lookup is constant-time regardless of stylesheet size
- RepaintBoundary per chunk —
ListView.buildersplits large documents into chunks, each with its own GPU layer; cross-chunk repaints never trigger - One-shot image listeners —
ImageStreamListenerself-removes on success and error; no listener leaks
HyperRender is a content renderer, not a browser. Use something else for:
| Need | Better Choice |
|---|---|
| Execute JavaScript | webview_flutter |
| Interactive web forms | webview_flutter |
| Rich text editing | super_editor, fleather |
position: fixed, canvas elements |
webview_flutter (via fallbackBuilder) |
| Maximum CSS property count, no float/CJK need | flutter_widget_from_html |
| Package | Purpose | Status |
|---|---|---|
hyper_render_core |
UDT model, CSS resolver, design tokens | ✅ Stable |
hyper_render_html |
HTML adapter | ✅ Stable |
hyper_render_markdown |
Markdown adapter | ✅ Stable |
hyper_render_highlight |
Syntax highlighting for <code> |
✅ Stable |
hyper_render_clipboard |
Image copy / share | ✅ Stable |
Use the convenience hyper_render package (this one) to get all of the above in one dependency.
CSS float · Flexbox · Grid · CSS Variables · calc() · Kinsoku + Ruby · <details> · RTL/BiDi · Quill Delta · Markdown · XSS sanitization · Screenshot export · HtmlHeuristics fallback · View virtualization · Design tokens · Dark mode · Performance monitoring
- Memory-pressure handling (
WidgetsBindingObserver— auto-evict caches on low-memory signal) - Full SVG renderer (currently shows placeholder)
video_player/audiointegration
hyper_render_mediapackage — extract media layer so core stays zero-dep- CSS
@keyframes/transitionexecution (currently parsed but not animated) hyper_render_devtoolsfirst working release (UDT inspector, computed-style panel)hyper_render_export— full-document PDF and high-res image export
Full details → doc/ROADMAP.md
git clone https://github.com/brewkits/hyper_render.git
cd hyper_render && flutter pub get
flutter test # all tests must pass
cd example && flutter run # run the demo appRead the Architecture Decision Records and Contributing Guide before submitting PRs.
MIT — see LICENSE.
If HyperRender saves you from WebView overhead, a star goes a long way:
HyperRender is designed and maintained by brewkits.
Built to solve a real problem: Flutter's rich-text ecosystem either uses WebView (heavy) or flutter_widget_from_html (no float, no CJK). HyperRender is the third option — a native single-RenderObject engine that runs at 60 FPS and handles real-world editorial HTML.
If this library helps your product ship faster, consider sponsoring development or leaving a ⭐ on GitHub.
Built with care for Flutter developers who push the platform.





