HyperRender
The Flutter HTML renderer that actually works.
60 FPS · 8 MB RAM · CSS float · Ruby typography · XSS-safe by default
CSS float layout · crash-free text selection · CJK/Furigana · @keyframes · Flexbox/Grid
Drop-in replacement for flutter_html and flutter_widget_from_html.
Quick Start · Why Switch? · API · Packages
Demos
| CSS Float Layout | Ruby / Furigana | Crash-Free Selection |
|---|---|---|
![]() |
![]() |
![]() |
| Text wraps around floated images — no other Flutter HTML renderer does this | Furigana centered above base glyphs, full Kinsoku line-breaking | Select across headings, paragraphs, tables — tested to 100 000 chars |
| Advanced Tables | Head-to-Head | Virtualized Mode |
|---|---|---|
![]() |
![]() |
![]() |
colspan · rowspan · W3C 2-pass column algorithm |
Same HTML in HyperRender vs flutter_widget_from_html | Virtualized rendering — 60 FPS on documents of any length |
Quick Start
dependencies:
hyper_render: ^1.1.3
import 'package:hyper_render/hyper_render.dart';
HyperViewer(
html: articleHtml,
onLinkTap: (url) => launchUrl(Uri.parse(url)),
)
Zero configuration. XSS sanitization is on by default. Works for articles, emails, docs, newsletters, and CJK content.
Why Switch? The Architecture Argument
Most Flutter HTML libraries map each HTML tag to a Flutter widget. A 3 000-word article becomes 500+ nested widgets — and some layout primitives simply cannot be expressed that way:
CSS
floatis not possible in a widget tree. Wrapping text around a floated image requires every fragment's coordinates before adjacent text can be composed. That geometry only exists when a singleRenderObjectowns the entire layout.
HyperRender renders the whole document inside one custom RenderObject. Float, crash-free selection, and sub-millisecond binary-search hit-testing all follow from that single design decision.
Feature Matrix
| Feature | flutter_html |
flutter_widget_from_html |
HyperRender |
|---|---|---|---|
float: left / right |
❌ | ❌ | ✅ |
| Text selection — large docs | ❌ Crashes | ❌ Crashes | ✅ Crash-free |
| Ruby / Furigana | ❌ Raw text | ❌ Raw text | ✅ |
<details> / <summary> |
❌ | ❌ | ✅ Interactive |
CSS Variables var() |
❌ | ❌ | ✅ |
CSS @keyframes |
❌ | ❌ | ✅ |
| Flexbox / Grid | ⚠️ Partial | ⚠️ Partial | ✅ Full |
Box shadow · filter |
❌ | ❌ | ✅ |
SVG <img src="*.svg"> |
⚠️ | ⚠️ | ✅ |
| Scroll FPS (25 K-char doc) | ~35 | ~45 | 60 |
| RAM (same doc) | 28 MB | 15 MB | 8 MB |
Benchmarks
Measured on iPhone 13 + Pixel 6 with a 25 000-character article:
| Metric | flutter_html |
flutter_widget_from_html |
HyperRender |
|---|---|---|---|
| Widgets created | ~600 | ~500 | 3–5 chunks |
| First parse | 420 ms | 250 ms | 95 ms |
| Peak RAM | 28 MB | 15 MB | 8 MB |
| Scroll FPS | ~35 | ~45 | 60 |
Features
CSS Float — Magazine Layouts
HyperViewer(html: '''
<article>
<img src="photo.jpg" style="float:left; width:180px; margin:0 16px 8px 0; border-radius:8px;" />
<h2>The Art of Layout</h2>
<p>Text wraps around the image exactly like a browser — because HyperRender
uses the same block formatting context algorithm.</p>
</article>
''')
Crash-Free Text Selection
HyperViewer(
html: longArticleHtml,
selectable: true,
showSelectionMenu: true,
selectionHandleColor: Colors.blue,
)
One continuous span tree. Selection crosses headings, paragraphs, and table cells. O(log N) binary-search hit-testing stays instant on 1 000-line documents.
CJK Typography — Ruby / Furigana
HyperViewer(html: '''
<p style="font-size:20px; line-height:2;">
<ruby>東京<rt>とうきょう</rt></ruby>で
<ruby>日本語<rt>にほんご</rt></ruby>を学ぶ
</p>
''')
Furigana centered above base characters. Kinsoku shori applied across the full line.
Ruby copied to clipboard as 東京(とうきょう).
CSS Variables · Flexbox · Grid
HyperViewer(html: '''
<style>
:root { --brand: #6750A4; --surface: #F3EFF4; }
</style>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
<div style="background:var(--brand); color:white; padding:16px; border-radius:12px;">
Column one — themed with CSS custom properties
</div>
<div style="background:var(--surface); padding:16px; border-radius:12px;">
Column two — same token system
</div>
</div>
''')
CSS @keyframes Animation
<style>
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideUp { from { transform: translateY(24px); opacity: 0; }
to { transform: translateY(0); opacity: 1; } }
.hero { animation: fadeIn 0.6s ease-out; }
.card { animation: slideUp 0.4s ease-out; }
</style>
<div class="hero"><h1>Welcome</h1></div>
<div class="card"><p>Animated without any Dart code.</p></div>
Parsed from <style> tags automatically — supports opacity, transform, vendor-prefixed variants,
and percentage selectors.
XSS Sanitization — Safe by Default
// Safe — strips <script>, on* handlers, javascript: URLs
HyperViewer(html: userGeneratedContent)
// Custom allowlist for stricter sandboxing
HyperViewer(html: userContent, allowedTags: ['p', 'a', 'img', 'strong', 'em'])
// Disable only for fully trusted, internal HTML
HyperViewer(html: trustedCmsHtml, sanitize: false)
Multi-Format Input
HyperViewer(html: '<h1>Hello</h1><p>World</p>')
HyperViewer.delta(delta: '{"ops":[{"insert":"Hello\\n"}]}')
HyperViewer.markdown(markdown: '# Hello\n\n**Bold** and _italic_.')
Screenshot Export
final captureKey = GlobalKey();
HyperViewer(html: articleHtml, captureKey: captureKey)
// Export to PNG bytes
final png = await captureKey.toPngBytes();
// Export with custom pixel ratio
final hd = await captureKey.toPngBytes(pixelRatio: 3.0);
Hybrid WebView Fallback
HyperViewer(
html: maybeComplexHtml,
fallbackBuilder: (context) => WebViewWidget(controller: _webViewController),
)
API Reference
HyperViewer
HyperViewer({
required String html,
String? baseUrl, // resolves relative <img src> and <a href>
String? customCss, // injected after the document's own <style> tags
bool selectable = true,
bool sanitize = true,
List<String>? allowedTags,
HyperRenderMode mode = HyperRenderMode.auto, // sync | virtualized | auto
void Function(String)? onLinkTap,
HyperWidgetBuilder? widgetBuilder, // custom widget injection
WidgetBuilder? fallbackBuilder,
WidgetBuilder? placeholderBuilder,
GlobalKey? captureKey,
bool showSelectionMenu = true,
String? semanticLabel,
HyperViewerController? controller,
void Function(Object, StackTrace)? onError,
})
HyperViewer.delta(delta: jsonString, ...)
HyperViewer.markdown(markdown: markdownString, ...)
HyperViewerController
final ctrl = HyperViewerController();
HyperViewer(html: html, controller: ctrl)
ctrl.jumpToAnchor('section-2'); // scroll to <a name="section-2">
ctrl.scrollToOffset(1200); // absolute pixel offset
Custom Widget Injection
Replace any HTML element with an arbitrary Flutter widget:
HyperViewer(
html: html,
widgetBuilder: (context, node) {
if (node is AtomicNode && node.tagName == 'iframe') {
return YoutubePlayer(url: node.attributes['src'] ?? '');
}
return null; // fall back to default rendering
},
)
HtmlHeuristics — Introspect Before Rendering
if (HtmlHeuristics.isComplex(html)) {
// use HyperRenderMode.virtualized for long documents
}
HtmlHeuristics.hasComplexTables(html)
HtmlHeuristics.hasUnsupportedCss(html)
HtmlHeuristics.hasUnsupportedElements(html)
Architecture
HTML / Markdown / Quill Delta
│
▼
ADAPTER LAYER HtmlAdapter · MarkdownAdapter · DeltaAdapter
│
▼
UNIFIED DOCUMENT TREE BlockNode · InlineNode · AtomicNode
RubyNode · TableNode · FlexContainerNode · GridNode
│
▼
CSS RESOLVER specificity cascade · var() · calc() · inheritance
│
▼
SINGLE RenderObject BFC · IFC · Float · Flexbox · Grid · Table
Canvas painting · continuous span tree
Kinsoku · O(log N) binary-search selection
Key engineering decisions:
- Single RenderObject — float layout and crash-free selection require one shared coordinate system; no widget-tree library can provide this
- O(1) CSS rule lookup — rules are indexed by tag / class / ID; constant time regardless of stylesheet size
- O(log N) hit-testing —
_lineStartOffsets[]precomputed at layout time; each touch is a binary search, not a linear scan - RepaintBoundary per chunk — each
ListView.builderchunk gets its own GPU layer; unmodified chunks are composited, not repainted
When NOT to Use
| Need | Better choice |
|---|---|
| Execute JavaScript | webview_flutter |
| Interactive web forms / input | webview_flutter |
| Rich text editing | super_editor, fleather |
position: fixed, <canvas>, media queries |
webview_flutter (use fallbackBuilder) |
| Maximum CSS coverage, float/CJK not required | flutter_widget_from_html |
Packages
| Package | pub.dev | Description |
|---|---|---|
hyper_render |
Convenience wrapper — one dependency, everything included | |
hyper_render_core |
Core engine — UDT model, CSS resolver, RenderObject, design tokens | |
hyper_render_html |
HTML + CSS parser | |
hyper_render_markdown |
Markdown adapter | |
hyper_render_highlight |
Syntax highlighting for <code> / <pre> blocks |
|
hyper_render_clipboard |
Image copy / share via super_clipboard |
|
hyper_render_devtools |
Flutter DevTools extension — UDT inspector, computed styles, demo mode |
Contributing
git clone https://github.com/brewkits/hyper_render.git
cd hyper_render
flutter pub get
flutter test
dart format --set-exit-if-changed .
flutter analyze --fatal-infos
Read the Architecture Decision Records and Contributing Guide before submitting a PR.
License
MIT — see LICENSE.
Libraries
- hyper_render
- HyperRender - The Universal Content Engine for Flutter





